pi-onlyne 1.2.1 → 1.2.2

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
@@ -414,7 +414,7 @@ path the client's daemon bound, read when the environment carried none, §8).
414
414
  | `reconnecting in 4000ms` in a loop | the client is down or the socket was replaced | `onlyne --server-root … roles` |
415
415
  | `ready refused: internal: unknown session for …` | the plugin mounted and reported for a task the client never staged (normal when pi is started by hand outside a task) | start pi under the client, not by hand |
416
416
  | `assign` never arrives | the client's `session_command` did not spawn pi, or `inject` was dropped | the client log for the spawn line; `/onlyne status` for the capability set |
417
- | ledger stays `in_flight` | no completion yet: no turn has run (the injected message has not executed), or the ladder is still reminding it (`idleReminders`) | the pi session file for the `onlyne-assign` entry and the reminders injected after it; the `onlyne` panel for `reminder n of m`; `/onlyne status` for the task and phase |
417
+ | ledger stays `in_flight` | no completion yet: no turn has run (the injected message has not executed), or the ladder is still reminding it (`idleReminders`). On pi-onlyne 1.2.1 against pi 0.87 an envelope carrying an image left nothing injected at all: pi read the flat `ImageContent` this plugin now sends, refused the nested part the older plugin built, and took the task text down with it | the pi session file for the `onlyne-assign` entry and the reminders injected after it; the `onlyne` panel for `reminder n of m`; `/onlyne status` for the task and phase; for that refusal, the pane's `Extension "<runtime>" error` line and, on 1.2.2, the plugin's `attachment carried no base64 data or no media type` log |
418
418
  | `onlyne_complete` answers `relay guard: missing handoff to: …` | the workspace's spec (or a `relay.toml` standing in for it) names a role this session never sent to | routine notices appear in the `onlyne` panel; stderr keeps refusals such as `relay guard from …`, socket errors, timeouts and framing faults; `required=…` names the policy; `relay guard: missing handoff …` names the delivered set |
419
419
  | `hello … forbidden` / connection closed right after `hello` | the mount role does not match the client's role | `hello.args.mount.role` vs the workspace's role |
420
420
  | `frame_too_large` | a body above 8 MiB | only reachable through an oversize outbound image; the ceiling is the core's |
@@ -695,7 +695,7 @@ relay_required = ["writer"] # these roles must have received a handoff
695
695
  | `reconnecting in 4000ms` 持续循环 | 客户端已停止,或套接字已被替换 | `onlyne --server-root … roles` |
696
696
  | `ready refused: internal: unknown session for …` | 插件完成挂载并为一个客户端从未暂存的任务进行了报告(在任务之外手动启动 pi 时属于正常情况) | 在客户端下启动 pi,不手动启动 |
697
697
  | `assign` 始终未到达 | 客户端的 `session_command` 未启动 pi,或 `inject` 已被移除 | 在客户端日志中查看启动行;通过 `/onlyne status` 查看能力集合 |
698
- | 账本保持 `in_flight` | 尚未完成:还没有轮次运行(注入消息尚未执行),或者阶梯仍在提醒(`idleReminders`) | 在 pi 会话文件中查看 `onlyne-assign` 条目及其后注入的提醒;通过 `onlyne` 面板查看 `reminder n of m`;通过 `/onlyne status` 查看任务和阶段 |
698
+ | 账本保持 `in_flight` | 尚未完成:还没有轮次运行(注入消息尚未执行),或者阶梯仍在提醒(`idleReminders`)。在 pi-onlyne 1.2.1 对上 pi 0.87 时,带图片附件的信封会什么都没注入:pi 读取本插件现在送出的扁平 `ImageContent`,拒掉旧插件构造的嵌套 part,任务正文随之一起丢失 | 在 pi 会话文件中查看 `onlyne-assign` 条目及其后注入的提醒;通过 `onlyne` 面板查看 `reminder n of m`;通过 `/onlyne status` 查看任务和阶段;要找那次拒绝,看窗格里的 `Extension "<runtime>" error` 行,以及 1.2.2 上插件的 `attachment carried no base64 data or no media type` 日志 |
699
699
  | `onlyne_complete` 回复 `relay guard: missing handoff to: …` | 工作区的规范(或替代该规范的 `relay.toml`)指定了一个此会话从未向其发送的角色 | 常规通知会出现在 `onlyne` 面板中;stderr 会保留拒绝消息,例如 `relay guard from …`、套接字错误、超时和分帧错误;`required=…` 指明策略;`relay guard: missing handoff …` 指明已送达的集合 |
700
700
  | `hello … forbidden`/在 `hello` 后立即关闭连接 | 挂载角色与客户端角色不匹配 | `hello.args.mount.role` 与工作区角色 |
701
701
  | `frame_too_large` | 正文超过 8 MiB | 仅可通过超大的出站图像达到;此上限来自核心 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-onlyne",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Onlyne agent adapter for pi: the session lifecycle an onlyne role client expects from a pi host.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,12 +52,6 @@ test("hostile input stays inside render bounds", () => {
52
52
  for (const line of lines) assert.ok(Array.from(line).length <= MAX_WIDTH, line);
53
53
  });
54
54
 
55
- test("identical state gives identical output", () => {
56
- const activity = createActivity({ clock }).set({ role: "planner", connection: "connected" });
57
- activity.note("in", "build it");
58
- assert.deepEqual(activity.lines(), activity.lines());
59
- });
60
-
61
55
  test("default render shows the newest SHOW_EVENTS events", () => {
62
56
  const activity = createActivity({ clock });
63
57
  for (let index = 0; index < SHOW_EVENTS + 4; index += 1) activity.note("state", `event ${index}`);
@@ -11,7 +11,6 @@ import { fileURLToPath } from "node:url";
11
11
  import { afterEach, test } from "node:test";
12
12
 
13
13
  import { OnlyneAgent } from "./agent.mjs";
14
- import { MAX_LINES, MAX_WIDTH } from "./activity.mjs";
15
14
  import { DEFAULT_IDLE_REMINDERS } from "./config.mjs";
16
15
  import { createFrameDecoder, encodeFrame } from "./frame.mjs";
17
16
  import { SEQ_BASE, readyReport } from "./protocol.mjs";
@@ -374,24 +373,6 @@ test("a reconnect reports the same pane", async () => {
374
373
  });
375
374
  });
376
375
 
377
- test("a second task in the same pane reports the same pane", async () => {
378
- await inOrcaPane(PANE_ENV, async () => {
379
- const { host } = await mountedAgent();
380
- host.notify("assign", assignArgs());
381
- await waitFor(() => (host.of("assign_ack").length === 1 ? true : null));
382
- host.notify("probe", {});
383
- await waitFor(() => (heartbeats(host).length ? true : null));
384
-
385
- // The binding belongs to the process, not to the task: handing this pane
386
- // another task cannot change where the process runs.
387
- host.notify("assign", { ...assignArgs(), task_id: "22222222-2222-4222-8222-222222222222" });
388
- await waitFor(() => (host.of("assign_ack").length === 2 ? true : null));
389
- host.notify("probe", {});
390
- const reports = await waitFor(() => (heartbeats(host).length >= 2 ? heartbeats(host) : null));
391
- assert.deepEqual(reports.at(-1).host, reports[0].host);
392
- });
393
- });
394
-
395
376
  test("a pi outside an Orca pane reports no host at all", async () => {
396
377
  await inOrcaPane({}, async () => {
397
378
  const { host } = await mountedAgent();
@@ -405,22 +386,6 @@ test("a pi outside an Orca pane reports no host at all", async () => {
405
386
  });
406
387
  });
407
388
 
408
- test("a pane that exports no handle reports the pane without inventing one", async () => {
409
- await inOrcaPane({ ORCA_PANE_KEY: PANE_KEY }, async () => {
410
- const { host } = await mountedAgent();
411
- host.notify("assign", assignArgs());
412
- await waitFor(() => (host.of("assign_ack").length === 1 ? true : null));
413
- host.notify("probe", {});
414
-
415
- const [observed] = await waitFor(() => (heartbeats(host).length ? heartbeats(host) : null));
416
- // The pane key carries both ids; the handle is a field the environment
417
- // either has or has not, and a missing one is absent rather than empty.
418
- assert.deepEqual(observed.host, {
419
- orca: { pane_key: PANE_KEY, tab_id: PANE_TAB, leaf_id: PANE_LEAF },
420
- });
421
- });
422
- });
423
-
424
389
  test("the binding is the environment the process was spawned with", async () => {
425
390
  await inOrcaPane(PANE_ENV, async () => {
426
391
  const { host } = await mountedAgent();
@@ -687,26 +652,6 @@ test("a busy pi holds the settle decision until it is waiting for input", async
687
652
  assert.match(reminded[1].text, /build it/, "the decision waits for the idle it is about");
688
653
  });
689
654
 
690
- test("queued input keeps the session non-idle and holds the ladder off", async () => {
691
- let queued = false;
692
- const surface = fakeSurface({ waiting: () => !queued });
693
- const { agent, host } = await startAgent({ surface, options: { settleFallbackMs: 40 } });
694
- agent.start();
695
- await waitFor(() => host.of("report").length >= 1);
696
- host.notify("assign", assignArgs());
697
- await waitFor(() => (surface.calls.wakeUser.length === 1 ? true : null));
698
-
699
- agent.onTurnStart();
700
- queued = true;
701
- agent.onTurnEnd();
702
- agent.onSettled();
703
- await waitFor(() => heartbeats(host).at(-1)?.agent === "running");
704
- await new Promise((resolve) => setTimeout(resolve, 80));
705
-
706
- assert.equal(surface.calls.wakeUser.length, 1, "a queued continuation is not an idle episode");
707
- assert.deepEqual(surface.calls.exits, []);
708
- });
709
-
710
655
  test("a live background task holds the ladder off until its task is terminal", async () => {
711
656
  let backgroundWork = true;
712
657
  const surface = fakeSurface({ backgroundWork: () => backgroundWork });
@@ -968,24 +913,6 @@ test("the exit waits for the client's acknowledgement of the completion report",
968
913
  assert.deepEqual(surface.calls.exits, ["done"]);
969
914
  });
970
915
 
971
- test("the completion report is the last frame the host receives before the exit", async () => {
972
- const { agent, host, surface } = await startAgent();
973
- agent.start();
974
- await waitFor(() => host.of("report").length >= 1);
975
- host.notify("assign", assignArgs());
976
- await waitFor(() => (surface.calls.wakeUser.length === 1 ? true : null));
977
- agent.onTurnStart();
978
- await waitFor(() =>
979
- host.of("report").find((report) => report.data?.observed?.agent === "running"),
980
- );
981
- agent.noteAssistantText("OK");
982
-
983
- await agent.complete(TASK_ID, "done", "OK");
984
- const reports = host.of("report").filter((report) => report.kind !== "ready");
985
- assert.equal(reports.at(-1).kind, "complete", "a running beat is not followed by a settled observation");
986
- assert.deepEqual(surface.calls.exits, ["done"]);
987
- });
988
-
989
916
  test("a reconnect beat re-derives the phase from the surface", async () => {
990
917
  let waiting = true;
991
918
  const surface = fakeSurface({ waiting: () => waiting });
@@ -1442,27 +1369,6 @@ test("a repeating fault folds into one panel line with a count", () => {
1442
1369
  assert.equal(surface.calls.widget.length, 3, "every event redraws the same panel");
1443
1370
  });
1444
1371
 
1445
- test("the panel stays inside its line and width budget", () => {
1446
- const surface = fakeSurface({ widget: true });
1447
- const agent = new OnlyneAgent({
1448
- socketPath: join(tmpdir(), "pi-onlyne-unused-socket"),
1449
- cwd: tmpdir(),
1450
- role: "planner",
1451
- sessionId: SESSION_ID,
1452
- taskId: TASK_ID,
1453
- surface,
1454
- log: () => {},
1455
- });
1456
- for (let index = 0; index < 40; index += 1) {
1457
- agent.notice("in", `task ${index} from gateway:wechat:room ${"长".repeat(400)}`);
1458
- }
1459
- const [render] = surface.calls.widget.slice(-1);
1460
- assert.ok(render.length <= MAX_LINES, `panel grew to ${render.length} lines`);
1461
- for (const line of render) {
1462
- assert.ok(Array.from(line).length <= MAX_WIDTH, `line overruns its width: ${line}`);
1463
- }
1464
- });
1465
-
1466
1372
  test("a host without the widget keeps the footer line and the stderr line", async () => {
1467
1373
  const surface = fakeSurface();
1468
1374
  const { agent, host, logs } = await startAgent({ surface });
@@ -8,8 +8,6 @@ import assert from "node:assert/strict";
8
8
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
- import { readFileSync } from "node:fs";
12
- import { fileURLToPath } from "node:url";
13
11
  import { afterEach, test } from "node:test";
14
12
 
15
13
  import { CONFIG_RELATIVE_PATH, DEFAULT_CONFIG, loadConfig, sessionIdentity } from "./config.mjs";
@@ -80,9 +78,3 @@ test("the session identity gate needs all three variables", () => {
80
78
  assert.equal(sessionIdentity({}), null);
81
79
  assert.equal(sessionIdentity({ ONLYNE_ROLE: "", ONLYNE_SESSION_ID: "s", ONLYNE_TASK_ID: "t" }), null);
82
80
  });
83
-
84
- test("the shipped switch file documents only the keys the plugin reads", () => {
85
- const path = fileURLToPath(new URL("../onlyne.json.example", import.meta.url));
86
- const example = JSON.parse(readFileSync(path, "utf8"));
87
- assert.deepEqual(example, { enabled: true, watch: { autoStart: true } });
88
- });
@@ -2,7 +2,7 @@
2
2
  // through the pi extension API, with a probe for each optional member so an
3
3
  // older pi degrades instead of throwing.
4
4
  //
5
- // Probed members (measured against pi 0.85.1):
5
+ // Probed members (measured against pi 0.87.1):
6
6
  // wakeUser pi.sendUserMessage(content, { deliverAs: "followUp" })
7
7
  // proseContext pi.sendMessage({customType,...}, { deliverAs:"followUp", triggerTurn:false })
8
8
  // customEntry pi.appendEntry(customType, data)
@@ -83,19 +83,35 @@ export function createSurface({ pi, log, context }) {
83
83
  }
84
84
  };
85
85
 
86
- /** One pi user message; images ride along as pi image content parts. */
86
+ /**
87
+ * One pi user message; images ride along as pi image content parts.
88
+ *
89
+ * pi reads an image part as the flat `ImageContent` of its message types —
90
+ * `data` plus `mimeType` — and normalizes every part before the message is
91
+ * built, so a part missing either string stops the whole delivery inside pi.
92
+ * pi reports that failure in its own pane and hands nothing back to this
93
+ * plugin, so an unusable part is dropped here and the assignment still
94
+ * travels: the injection text already names the file that was written.
95
+ */
87
96
  const wakeUser = (text, parts = []) => {
88
97
  if (!available.wakeUser) {
89
98
  log("pi has no sendUserMessage; the assignment reached the session log only");
90
99
  return false;
91
100
  }
92
- const content = parts.length === 0
101
+ const images = parts.filter(
102
+ (part) => typeof part?.data === "string" && typeof part?.mime === "string" && part.mime,
103
+ );
104
+ if (images.length !== parts.length) {
105
+ log(`attachment carried no base64 data or no media type; ${parts.length - images.length} dropped, the task text still went`);
106
+ }
107
+ const content = images.length === 0
93
108
  ? text
94
109
  : [
95
110
  { type: "text", text },
96
- ...parts.map((part) => ({
111
+ ...images.map((part) => ({
97
112
  type: "image",
98
- source: { type: "base64", mediaType: part.mime, data: part.data },
113
+ data: part.data,
114
+ mimeType: part.mime,
99
115
  })),
100
116
  ];
101
117
  try {
@@ -0,0 +1,160 @@
1
+ // The pi-facing half of the surface: exactly what `wakeUser` hands
2
+ // `pi.sendUserMessage`. Every shape asserted here is pi's own declaration —
3
+ // `sendUserMessage(content: string | (TextContent | ImageContent)[])`, with
4
+ // `ImageContent` the flat `{ type: "image", data, mimeType }` of
5
+ // `@earendil-works/pi-ai` (measured against pi 0.87.1). pi normalizes each part
6
+ // before it builds the message, so a part in any other shape — the nested
7
+ // Anthropic `source: { type, media_type, data }` this plugin once sent — stops
8
+ // the whole delivery inside pi, and pi reports that in its own pane without
9
+ // handing anything back to the plugin.
10
+
11
+ import assert from "node:assert/strict";
12
+ import { test } from "node:test";
13
+
14
+ import { createSurface } from "./pi-surface.mjs";
15
+
16
+ /** The injection text `agent.mjs` builds for one assignment. */
17
+ const TASK_TEXT = "[onlyne] task 11111111-1111-4111-8111-111111111111 from role:planner";
18
+
19
+ /** One inline image part in the shape the agent hands the surface. */
20
+ const PNG_PART = { type: "image", mime: "image/png", data: "iVBORw0KGgoAAAANSUhEUg==", name: "shot.png" };
21
+
22
+ /** pi's own `UserMessage.content` for that text and that part, key for key. */
23
+ const PNG_CONTENT = [
24
+ { type: "text", text: TASK_TEXT },
25
+ { type: "image", data: "iVBORw0KGgoAAAANSUhEUg==", mimeType: "image/png" },
26
+ ];
27
+
28
+ /**
29
+ * A surface over a hand-written fake pi: every `sendUserMessage` arrives with
30
+ * the arguments it was given, recorded whole, and every surface log line with
31
+ * it. `refuse` makes one call throw synchronously, the way a pi that does not
32
+ * know an option does.
33
+ * @param {{ refuse?: (content: unknown, options: unknown) => boolean }} [options]
34
+ */
35
+ function fakeSurface({ refuse = null } = {}) {
36
+ const calls = [];
37
+ const lines = [];
38
+ const surface = createSurface({
39
+ pi: {
40
+ sendUserMessage(content, options) {
41
+ calls.push({ content, options });
42
+ if (refuse?.(content, options)) throw new Error("this pi does not take that option");
43
+ },
44
+ },
45
+ log: (line) => lines.push(line),
46
+ context: () => null,
47
+ });
48
+ return { calls, lines, surface };
49
+ }
50
+
51
+ test("an image attachment reaches pi as one flat image content part beside the task text", () => {
52
+ const { calls, lines, surface } = fakeSurface();
53
+
54
+ assert.equal(surface.wakeUser(TASK_TEXT, [PNG_PART]), true);
55
+ assert.equal(calls.length, 1, "one message, not one call per part");
56
+ // The whole array, key for key: a nested `source` / `mediaType` part carries
57
+ // keys pi does not declare and lacks the two it does, so it cannot pass this.
58
+ assert.deepEqual(calls[0].content, PNG_CONTENT);
59
+ assert.deepEqual(lines, [], "a readable attachment is delivered, not reported");
60
+ });
61
+
62
+ test("task text with no attachments reaches pi as a bare string", () => {
63
+ const { calls, surface } = fakeSurface();
64
+
65
+ assert.equal(surface.wakeUser(TASK_TEXT, []), true);
66
+ assert.equal(calls.length, 1);
67
+ // pi takes the string path of `content`; a one-element array of text parts is
68
+ // a different message and a needless normalization round trip.
69
+ assert.equal(calls[0].content, TASK_TEXT);
70
+ });
71
+
72
+ test("an attachment pi cannot read is dropped, logged, and the task text still travels", () => {
73
+ const { calls, lines, surface } = fakeSurface();
74
+
75
+ // Each part below fails one clause of what pi needs: no base64 data, no media
76
+ // type, an empty media type. Sending any of them stops the whole delivery
77
+ // inside pi, so all three go and the text goes alone.
78
+ assert.equal(
79
+ surface.wakeUser(TASK_TEXT, [
80
+ { type: "image", mime: "image/png", name: "no-data.png" },
81
+ { type: "image", data: "iVBORw0KGgoAAAANSUhEUg==", name: "no-mime.png" },
82
+ { type: "image", data: "iVBORw0KGgoAAAANSUhEUg==", mime: "" },
83
+ ]),
84
+ true,
85
+ );
86
+ assert.equal(calls.length, 1);
87
+ assert.equal(calls[0].content, TASK_TEXT);
88
+ assert.equal(lines.length, 1);
89
+ assert.match(lines[0], /3 dropped/);
90
+ assert.match(lines[0], /the task text still went/);
91
+
92
+ // Dropping is per part: a usable image beside an unusable one still rides.
93
+ assert.equal(surface.wakeUser(TASK_TEXT, [{ type: "image", mime: "image/png" }, PNG_PART]), true);
94
+ assert.deepEqual(calls[1].content, PNG_CONTENT);
95
+ assert.equal(lines.length, 2);
96
+ assert.match(lines[1], /1 dropped/);
97
+ });
98
+
99
+ test("delivery asks for followUp and a refusal of that option retries with the same content", () => {
100
+ const asked = fakeSurface();
101
+
102
+ assert.equal(asked.surface.wakeUser(TASK_TEXT, [PNG_PART]), true);
103
+ assert.deepEqual(asked.calls, [{ content: PNG_CONTENT, options: { deliverAs: "followUp" } }]);
104
+
105
+ // An older pi throws on the option it does not know. The message itself is
106
+ // fine, so the retry drops the option and keeps the content.
107
+ const refusing = fakeSurface({ refuse: (_content, options) => Boolean(options) });
108
+
109
+ assert.equal(refusing.surface.wakeUser(TASK_TEXT, [PNG_PART]), true);
110
+ assert.equal(refusing.calls.length, 2);
111
+ assert.deepEqual(refusing.calls[0].options, { deliverAs: "followUp" });
112
+ assert.equal(refusing.calls[1].options, undefined);
113
+ assert.deepEqual(refusing.calls[1].content, PNG_CONTENT);
114
+ assert.deepEqual(refusing.lines, [], "a delivery that landed on the retry is not a failure");
115
+ });
116
+
117
+ /**
118
+ * A surface over a fake pi that answers the two questions the phase is read
119
+ * from, plus the cases where pi answers neither.
120
+ * @param {{ isIdle?: () => boolean, hasPendingMessages?: () => boolean, throwOn?: "idle" | "pending" }} [pi]
121
+ */
122
+ function idleSurface(pi = {}) {
123
+ const lines = [];
124
+ return {
125
+ lines,
126
+ surface: createSurface({ pi, log: (line) => lines.push(line), context: () => pi }),
127
+ };
128
+ }
129
+
130
+ test("only a session waiting on its next message reads as idle", async () => {
131
+ const waiting = idleSurface({ isIdle: () => true, hasPendingMessages: () => false });
132
+ assert.equal(await waiting.surface.waitingForInput(), true);
133
+
134
+ // A queued continuation is input already on its way, so the session is not
135
+ // waiting for the user even though no turn is running.
136
+ const queued = idleSurface({ isIdle: () => true, hasPendingMessages: () => true });
137
+ assert.equal(await queued.surface.waitingForInput(), false);
138
+
139
+ // A turn in flight, a retry, and a compaction all read as not idle here.
140
+ const busy = idleSurface({ isIdle: () => false, hasPendingMessages: () => false });
141
+ assert.equal(await busy.surface.waitingForInput(), false);
142
+ });
143
+
144
+ test("a phase pi was never asked about reads as running, not idle", async () => {
145
+ // The probes are optional members of an older pi, and a session with no
146
+ // context yet has witnessed nothing: both answer `false` rather than guess.
147
+ const noProbes = idleSurface({});
148
+ assert.equal(await noProbes.surface.waitingForInput(), false);
149
+
150
+ const noContext = createSurface({ pi: { isIdle: () => true }, log: () => {}, context: () => null });
151
+ assert.equal(await noContext.waitingForInput(), false);
152
+
153
+ const throwing = idleSurface({
154
+ isIdle: () => true,
155
+ hasPendingMessages: () => {
156
+ throw new Error("pi is mid-replay");
157
+ },
158
+ });
159
+ assert.equal(await throwing.surface.waitingForInput(), false);
160
+ });
@@ -219,16 +219,6 @@ test("observationFor attaches the host only when there is one", () => {
219
219
  assert.deepEqual(dimensions, bare);
220
220
  });
221
221
 
222
- test("heartbeatReport carries the host inside observed", () => {
223
- const report = heartbeatReport({ taskId: "t1", generation: 1, seq: SEQ_BASE + 1, agent: "idle", host: hostBinding(PANE_ENV) });
224
- assert.equal(report.data.observed.host.orca.pane_key, PANE_KEY);
225
- assert.equal(report.data.observed.agent, "idle");
226
- assert.equal(
227
- "host" in heartbeatReport({ taskId: "t1", generation: 1, seq: SEQ_BASE + 2, agent: "idle" }).data.observed,
228
- false
229
- );
230
- });
231
-
232
222
  test("a completion names an outcome and a single-line head", () => {
233
223
  assert.deepEqual(completeReport({ taskId: "t1", outcome: "failed", head: "broke\non line two" }), {
234
224
  kind: "complete",