@openparachute/agent 0.2.2 → 0.2.3-rc.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.
@@ -117,10 +117,10 @@ describe("VaultTransport — reply (outbound note write)", () => {
117
117
  // a child-only-tagged note is invisible to a `tag:#agent/message` query), and
118
118
  // the directional child `#agent/message/outbound` is the trigger discriminator.
119
119
  // We WRITE only the `#agent/message*` tags.
120
- expect(sent.tags).toEqual(["#agent/message", "#agent/message/outbound"]);
120
+ expect(sent.tags).toEqual(["agent/message", "agent/message/outbound"]);
121
121
  // Regression guard: the queryable parent tag MUST be present literally.
122
- expect(sent.tags).toContain("#agent/message");
123
- expect(sent.tags).toContain("#agent/message/outbound");
122
+ expect(sent.tags).toContain("agent/message");
123
+ expect(sent.tags).toContain("agent/message/outbound");
124
124
  // Write-discipline: the interim/legacy tags are gone (CONTRACT dropped them).
125
125
  expect(sent.tags).not.toContain("#agent-message");
126
126
  expect(sent.tags).not.toContain("#agent-message/outbound");
@@ -246,8 +246,8 @@ describe("VaultTransport — writeThread (#agent/thread note, the unified model)
246
246
  // LOOP SAFETY (HARD CONSTRAINT 4): the thread note carries the thread tag EXACTLY —
247
247
  // NOT a message tag + NOT the inbound child — so it can never wake a session.
248
248
  expect(sent.tags).toEqual([AGENT_THREAD_TAG]);
249
- expect(sent.tags).not.toContain("#agent/message");
250
- expect(sent.tags).not.toContain("#agent/message/inbound");
249
+ expect(sent.tags).not.toContain("agent/message");
250
+ expect(sent.tags).not.toContain("agent/message/inbound");
251
251
  // Indexed/queryable fields.
252
252
  expect(sent.metadata.status).toBe("ok");
253
253
  expect(sent.metadata.definition).toBe("Agents/digest");
@@ -1007,7 +1007,7 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
1007
1007
  getUrls.push(u);
1008
1008
  capturedAuth = (init?.headers as Record<string, string> | undefined)?.authorization ?? "";
1009
1009
  // CONTRACT: a SINGLE `#agent/message` query (no interim/legacy union).
1010
- if (u.includes("tag=%23agent%2Fmessage")) {
1010
+ if (u.includes("tag=agent%2Fmessage")) {
1011
1011
  // Return notes OUT of ts order (prove the ascending sort) + a note from a
1012
1012
  // DIFFERENT channel (prove the client-side channel filter excludes it).
1013
1013
  return new Response(
@@ -1015,19 +1015,19 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
1015
1015
  {
1016
1016
  id: "n-out",
1017
1017
  content: "session reply",
1018
- tags: ["#agent/message", "#agent/message/outbound"],
1018
+ tags: ["agent/message", "agent/message/outbound"],
1019
1019
  metadata: { agent: "eng", direction: "outbound", sender: "session", ts: "2026-06-08T00:00:02Z", in_reply_to: "n-in" },
1020
1020
  },
1021
1021
  {
1022
1022
  id: "n-other",
1023
1023
  content: "different channel — must be excluded",
1024
- tags: ["#agent/message", "#agent/message/inbound"],
1024
+ tags: ["agent/message", "agent/message/inbound"],
1025
1025
  metadata: { agent: "other", direction: "inbound", sender: "x", ts: "2026-06-08T00:00:03Z" },
1026
1026
  },
1027
1027
  {
1028
1028
  id: "n-in",
1029
1029
  content: "hi session",
1030
- tags: ["#agent/message", "#agent/message/inbound"],
1030
+ tags: ["agent/message", "agent/message/inbound"],
1031
1031
  metadata: { agent: "eng", direction: "inbound", sender: "aaron", ts: "2026-06-08T00:00:01Z" },
1032
1032
  },
1033
1033
  ]),
@@ -1049,7 +1049,7 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
1049
1049
  // `metadata=` operator filter (the routing-key field isn't indexed on a bare
1050
1050
  // vault; we filter client-side). Overfetches the tag so other channels don't
1051
1051
  // crowd us out.
1052
- const agentGets = getUrls.filter((u) => u.includes("tag=%23agent%2Fmessage"));
1052
+ const agentGets = getUrls.filter((u) => u.includes("tag=agent%2Fmessage"));
1053
1053
  expect(agentGets).toHaveLength(1);
1054
1054
  // No interim/legacy queries are issued.
1055
1055
  expect(getUrls.some((u) => u.includes("tag=%23agent-message"))).toBe(false);
@@ -1076,12 +1076,12 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
1076
1076
  globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
1077
1077
  const u = String(url);
1078
1078
  if (u.includes("/api/notes") && (init?.method ?? "GET") === "GET") {
1079
- if (u.includes("tag=%23agent%2Fmessage")) {
1079
+ if (u.includes("tag=agent%2Fmessage")) {
1080
1080
  return new Response(
1081
1081
  JSON.stringify([1, 2, 3, 4].map((i) => ({
1082
1082
  id: "n" + i,
1083
1083
  content: "m" + i,
1084
- tags: ["#agent/message", "#agent/message/inbound"],
1084
+ tags: ["agent/message", "agent/message/inbound"],
1085
1085
  metadata: { agent: "eng", direction: "inbound", sender: "aaron", ts: "2026-06-08T00:00:0" + i + "Z" },
1086
1086
  }))),
1087
1087
  { status: 200 },
@@ -1102,13 +1102,13 @@ describe("VaultTransport — loadTranscript (read the durable store)", () => {
1102
1102
  globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
1103
1103
  const u = String(url);
1104
1104
  if (u.includes("/api/notes") && (init?.method ?? "GET") === "GET") {
1105
- if (u.includes("tag=%23agent%2Fmessage")) {
1105
+ if (u.includes("tag=agent%2Fmessage")) {
1106
1106
  return new Response(
1107
1107
  JSON.stringify([
1108
1108
  // Outbound child → direction inferred "outbound".
1109
- { id: "a", content: "x", tags: ["#agent/message", "#agent/message/outbound"], metadata: { agent: "eng", ts: "2026-06-08T00:00:01Z" } },
1109
+ { id: "a", content: "x", tags: ["agent/message", "agent/message/outbound"], metadata: { agent: "eng", ts: "2026-06-08T00:00:01Z" } },
1110
1110
  // No direction signal at all → defaults to "inbound".
1111
- { id: "b", content: "y", tags: ["#agent/message", "#agent/message/inbound"], metadata: { agent: "eng", ts: "2026-06-08T00:00:02Z" } },
1111
+ { id: "b", content: "y", tags: ["agent/message", "agent/message/inbound"], metadata: { agent: "eng", ts: "2026-06-08T00:00:02Z" } },
1112
1112
  ]),
1113
1113
  { status: 200 },
1114
1114
  );
@@ -1168,11 +1168,11 @@ describe("VaultTransport — writeInbound (the chat's send → wakes the session
1168
1168
  };
1169
1169
  expect(sent.content).toBe("wake up");
1170
1170
  // The INBOUND tag pair — the child is the trigger discriminator that wakes the session.
1171
- expect(sent.tags).toEqual(["#agent/message", "#agent/message/inbound"]);
1172
- expect(sent.tags).toContain("#agent/message");
1173
- expect(sent.tags).toContain("#agent/message/inbound");
1171
+ expect(sent.tags).toEqual(["agent/message", "agent/message/inbound"]);
1172
+ expect(sent.tags).toContain("agent/message");
1173
+ expect(sent.tags).toContain("agent/message/inbound");
1174
1174
  // It must NOT carry the outbound tag (that would be a reply, never wake).
1175
- expect(sent.tags).not.toContain("#agent/message/outbound");
1175
+ expect(sent.tags).not.toContain("agent/message/outbound");
1176
1176
  // Write-discipline: the legacy tag family is gone (CONTRACT dropped it).
1177
1177
  expect(sent.tags).not.toContain("#channel-message");
1178
1178
  // CONTRACT: the routing key under `metadata.agent` ONLY — no `channel`. The vault
@@ -1244,8 +1244,8 @@ describe("VaultTransport — writeCallback (agent-to-agent reply_to substrate)",
1244
1244
 
1245
1245
  expect(result.sent).toEqual(["callback-note-1"]);
1246
1246
  // The callback is an INBOUND note (so it wakes the sender via the normal vault trigger).
1247
- expect(sent!.tags).toEqual(["#agent/message", "#agent/message/inbound"]);
1248
- expect(sent!.tags).not.toContain("#agent/message/outbound");
1247
+ expect(sent!.tags).toEqual(["agent/message", "agent/message/inbound"]);
1248
+ expect(sent!.tags).not.toContain("agent/message/outbound");
1249
1249
  // The metadata contract — all present fields stamped.
1250
1250
  expect(sent!.metadata.callback).toBe("true");
1251
1251
  expect(sent!.metadata.status).toBe("ok");
@@ -1315,7 +1315,7 @@ describe("VaultTransport — ingestInbound", () => {
1315
1315
  void t.ingestInbound({
1316
1316
  id: "note-in-1",
1317
1317
  content: "hello session",
1318
- tags: ["#agent/message", "#agent/message/inbound"],
1318
+ tags: ["agent/message", "agent/message/inbound"],
1319
1319
  metadata: { agent: "eng", direction: "inbound", sender: "aaron", ts: "2026-06-08T00:00:00Z" },
1320
1320
  });
1321
1321
  expect(ctx.emitted).toHaveLength(1);
@@ -1341,7 +1341,7 @@ describe("VaultTransport — ingestInbound", () => {
1341
1341
  void t.ingestInbound({
1342
1342
  id: "our-own-reply",
1343
1343
  content: "I am awake",
1344
- tags: ["#agent/message", "#agent/message/outbound"],
1344
+ tags: ["agent/message", "agent/message/outbound"],
1345
1345
  metadata: { channel: "eng", direction: "outbound", sender: "session" },
1346
1346
  });
1347
1347
  expect(ctx.emitted).toHaveLength(0);
@@ -1382,7 +1382,7 @@ describe("VaultTransport — ingestInbound", () => {
1382
1382
  await t.ingestInbound({
1383
1383
  id: "note-att-1",
1384
1384
  content: "look at these",
1385
- tags: ["#agent/message", "#agent/message/inbound"],
1385
+ tags: ["agent/message", "agent/message/inbound"],
1386
1386
  metadata: { agent: "eng", direction: "inbound", sender: "aaron" },
1387
1387
  // inline list from the trigger payload — the has-attachments SIGNAL.
1388
1388
  attachments: [{ id: "a1", path: "2026-06-24/pic.png", mimeType: "image/png" }],
@@ -1408,7 +1408,7 @@ describe("VaultTransport — ingestInbound", () => {
1408
1408
  await t.ingestInbound({
1409
1409
  id: "note-att-fail",
1410
1410
  content: "still delivered",
1411
- tags: ["#agent/message", "#agent/message/inbound"],
1411
+ tags: ["agent/message", "agent/message/inbound"],
1412
1412
  metadata: { agent: "eng", direction: "inbound" },
1413
1413
  attachments: [{ id: "a1", path: "2026-06-24/pic.png", mimeType: "image/png" }],
1414
1414
  });
@@ -1429,7 +1429,7 @@ describe("VaultTransport — ingestInbound", () => {
1429
1429
  void t.ingestInbound({
1430
1430
  id: "note-plain",
1431
1431
  content: "no files",
1432
- tags: ["#agent/message", "#agent/message/inbound"],
1432
+ tags: ["agent/message", "agent/message/inbound"],
1433
1433
  metadata: { agent: "eng", direction: "inbound" },
1434
1434
  });
1435
1435
  expect(ctx.emitted).toHaveLength(1);
@@ -1447,7 +1447,7 @@ describe("VaultTransport — ingestInbound", () => {
1447
1447
  void t.ingestInbound({
1448
1448
  id: "note-deleg-1",
1449
1449
  content: "do the sub-task",
1450
- tags: ["#agent/message", "#agent/message/inbound"],
1450
+ tags: ["agent/message", "agent/message/inbound"],
1451
1451
  metadata: {
1452
1452
  channel: "worker",
1453
1453
  direction: "inbound",
@@ -1482,10 +1482,11 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
1482
1482
 
1483
1483
  expect(calls).toHaveLength(AGENT_VAULT_TAG_SCHEMA.length);
1484
1484
 
1485
- // Namespace ROOT `#agent` — no parent_names, just a description. Plain `#` → `%23`.
1485
+ // Namespace ROOT `agent` — no parent_names, just a description. A single bare
1486
+ // segment needs no percent-encoding.
1486
1487
  const root = calls[0]!;
1487
1488
  expect(root.url).toBe(
1488
- "http://127.0.0.1:1940/vault/default/api/tags/%23agent",
1489
+ "http://127.0.0.1:1940/vault/default/api/tags/agent",
1489
1490
  );
1490
1491
  expect(root.init.method).toBe("PUT");
1491
1492
  expect((root.init.headers as Record<string, string>).authorization).toBe(
@@ -1497,19 +1498,19 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
1497
1498
  };
1498
1499
  expect("parent_names" in rootBody).toBe(false);
1499
1500
 
1500
- // Definition (NEW) — name carries `#` + `/`; rolls up to the namespace root.
1501
+ // Definition (NEW) — name carries `/`; rolls up to the namespace root.
1501
1502
  const def = calls[1]!;
1502
1503
  expect(def.url).toBe(
1503
- "http://127.0.0.1:1940/vault/default/api/tags/%23agent%2Fdefinition",
1504
+ "http://127.0.0.1:1940/vault/default/api/tags/agent%2Fdefinition",
1504
1505
  );
1505
- expect(decodeURIComponent(def.url.split("/api/tags/")[1]!)).toBe("#agent/definition");
1506
+ expect(decodeURIComponent(def.url.split("/api/tags/")[1]!)).toBe("agent/definition");
1506
1507
  const defBody = JSON.parse(String(def.init.body)) as { parent_names?: string[] };
1507
- expect(defBody.parent_names).toEqual(["#agent"]);
1508
+ expect(defBody.parent_names).toEqual(["agent"]);
1508
1509
 
1509
1510
  // Message parent (NEW) — rolls up to the namespace root.
1510
1511
  const parent = calls[2]!;
1511
1512
  expect(parent.url).toBe(
1512
- "http://127.0.0.1:1940/vault/default/api/tags/%23agent%2Fmessage",
1513
+ "http://127.0.0.1:1940/vault/default/api/tags/agent%2Fmessage",
1513
1514
  );
1514
1515
  const parentBody = JSON.parse(String(parent.init.body)) as {
1515
1516
  description?: string;
@@ -1518,23 +1519,23 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
1518
1519
  expect(parentBody.description).toBe(
1519
1520
  "A message in a Parachute channel (parent of /inbound + /outbound).",
1520
1521
  );
1521
- expect(parentBody.parent_names).toEqual(["#agent"]);
1522
+ expect(parentBody.parent_names).toEqual(["agent"]);
1522
1523
 
1523
- // Inbound child (NEW) — name carries BOTH `#` and `/`. The vault route matches a
1524
+ // Inbound child (NEW) — name carries `/`. The vault route matches a
1524
1525
  // single path segment (`[^/]+`) then decodeURIComponent's it, so the `/` MUST
1525
1526
  // be encoded as `%2F` (a bare slash would fail the single-segment match → 404).
1526
1527
  const inbound = calls[3]!;
1527
1528
  expect(inbound.url).toBe(
1528
- "http://127.0.0.1:1940/vault/default/api/tags/%23agent%2Fmessage%2Finbound",
1529
+ "http://127.0.0.1:1940/vault/default/api/tags/agent%2Fmessage%2Finbound",
1529
1530
  );
1530
1531
  // Confirm the encoding decodes back to the literal tag name the vault stores.
1531
1532
  const encodedSegment = inbound.url.split("/api/tags/")[1]!;
1532
- expect(decodeURIComponent(encodedSegment)).toBe("#agent/message/inbound");
1533
+ expect(decodeURIComponent(encodedSegment)).toBe("agent/message/inbound");
1533
1534
  const inboundBody = JSON.parse(String(inbound.init.body)) as {
1534
1535
  description?: string;
1535
1536
  parent_names?: string[];
1536
1537
  };
1537
- expect(inboundBody.parent_names).toEqual(["#agent/message"]);
1538
+ expect(inboundBody.parent_names).toEqual(["agent/message"]);
1538
1539
  expect(inboundBody.description).toBe(
1539
1540
  "Human→session message; the vault trigger fires on this.",
1540
1541
  );
@@ -1542,19 +1543,19 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
1542
1543
  // Outbound child (NEW) — same encoding, parent declared.
1543
1544
  const outbound = calls[4]!;
1544
1545
  expect(outbound.url).toBe(
1545
- "http://127.0.0.1:1940/vault/default/api/tags/%23agent%2Fmessage%2Foutbound",
1546
+ "http://127.0.0.1:1940/vault/default/api/tags/agent%2Fmessage%2Foutbound",
1546
1547
  );
1547
1548
  expect(decodeURIComponent(outbound.url.split("/api/tags/")[1]!)).toBe(
1548
- "#agent/message/outbound",
1549
+ "agent/message/outbound",
1549
1550
  );
1550
1551
  const outboundBody = JSON.parse(String(outbound.init.body)) as { parent_names?: string[] };
1551
- expect(outboundBody.parent_names).toEqual(["#agent/message"]);
1552
+ expect(outboundBody.parent_names).toEqual(["agent/message"]);
1552
1553
 
1553
1554
  // Job (NEW) — rolls up to the namespace root.
1554
1555
  const job = calls[5]!;
1555
- expect(decodeURIComponent(job.url.split("/api/tags/")[1]!)).toBe("#agent/job");
1556
+ expect(decodeURIComponent(job.url.split("/api/tags/")[1]!)).toBe("agent/job");
1556
1557
  const jobBody = JSON.parse(String(job.init.body)) as { parent_names?: string[] };
1557
- expect(jobBody.parent_names).toEqual(["#agent"]);
1558
+ expect(jobBody.parent_names).toEqual(["agent"]);
1558
1559
  });
1559
1560
 
1560
1561
  test("schema declares ONLY the #agent/* namespace rollup (CONTRACT dropped interim + legacy, 7 entries)", async () => {
@@ -1564,29 +1565,29 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
1564
1565
  // schema entries — exactly 7 entries, all under `#agent/*`.
1565
1566
  const names = AGENT_VAULT_TAG_SCHEMA.map((e) => e.name);
1566
1567
  expect(names).toEqual([
1567
- "#agent",
1568
- "#agent/definition",
1569
- "#agent/message",
1570
- "#agent/message/inbound",
1571
- "#agent/message/outbound",
1572
- "#agent/job",
1573
- "#agent/thread",
1568
+ "agent",
1569
+ "agent/definition",
1570
+ "agent/message",
1571
+ "agent/message/inbound",
1572
+ "agent/message/outbound",
1573
+ "agent/job",
1574
+ "agent/thread",
1574
1575
  ]);
1575
1576
  // The interim/legacy families are gone entirely.
1576
1577
  expect(names).not.toContain("#agent-message");
1577
1578
  expect(names).not.toContain("#channel-message");
1578
1579
  // The namespace children all roll up to the `#agent` root (the human rollup).
1579
1580
  const byName = (n: string) => AGENT_VAULT_TAG_SCHEMA.find((e) => e.name === n)!;
1580
- expect(byName("#agent/definition").parent_names).toEqual(["#agent"]);
1581
- expect(byName("#agent/message").parent_names).toEqual(["#agent"]);
1582
- expect(byName("#agent/job").parent_names).toEqual(["#agent"]);
1583
- expect(byName("#agent/thread").parent_names).toEqual(["#agent"]);
1584
- expect(byName("#agent/message/inbound").parent_names).toEqual(["#agent/message"]);
1585
- expect(byName("#agent/message/outbound").parent_names).toEqual(["#agent/message"]);
1581
+ expect(byName("agent/definition").parent_names).toEqual(["agent"]);
1582
+ expect(byName("agent/message").parent_names).toEqual(["agent"]);
1583
+ expect(byName("agent/job").parent_names).toEqual(["agent"]);
1584
+ expect(byName("agent/thread").parent_names).toEqual(["agent"]);
1585
+ expect(byName("agent/message/inbound").parent_names).toEqual(["agent/message"]);
1586
+ expect(byName("agent/message/outbound").parent_names).toEqual(["agent/message"]);
1586
1587
  // `#agent/thread` declares INDEXED string fields so threads are operator-queryable —
1587
1588
  // "all failed threads" (status), "all threads of agent X" (definition), "all
1588
1589
  // multi-threaded threads" (mode). The three axes carry over from the run record VERBATIM.
1589
- expect(byName("#agent/thread").fields).toEqual({
1590
+ expect(byName("agent/thread").fields).toEqual({
1590
1591
  // The canonical `agent` routing-key alias is declared indexed.
1591
1592
  agent: { type: "string", indexed: true },
1592
1593
  status: { type: "string", indexed: true },
@@ -1594,11 +1595,11 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
1594
1595
  mode: { type: "string", indexed: true },
1595
1596
  });
1596
1597
  // `#agent/message` declares the indexed `agent` routing key.
1597
- expect(byName("#agent/message").fields).toEqual({
1598
+ expect(byName("agent/message").fields).toEqual({
1598
1599
  agent: { type: "string", indexed: true },
1599
1600
  });
1600
1601
  // CONTRACT: `#agent/job` indexes the routing key under `agent` ONLY — no `channel`.
1601
- expect(byName("#agent/job").fields).toEqual({
1602
+ expect(byName("agent/job").fields).toEqual({
1602
1603
  agent: { type: "string", indexed: true },
1603
1604
  enabled: { type: "string", indexed: true },
1604
1605
  lastStatus: { type: "string", indexed: true },
@@ -1691,7 +1692,7 @@ describe("VaultTransport — ensureSchema (tag-schema declaration on connect)",
1691
1692
  void t.ingestInbound({
1692
1693
  id: "n1",
1693
1694
  content: "still works",
1694
- tags: ["#agent/message", "#agent/message/inbound"],
1695
+ tags: ["agent/message", "agent/message/inbound"],
1695
1696
  metadata: { channel: "eng", direction: "inbound", sender: "aaron" },
1696
1697
  });
1697
1698
  expect(ctx.emitted).toHaveLength(1);
@@ -1741,7 +1742,7 @@ describe("VaultTransport — injectInbound (runner seam)", () => {
1741
1742
  expect(noteCalls).toHaveLength(1);
1742
1743
  const body = JSON.parse(String(noteCalls[0]!.init.body));
1743
1744
  // Inbound: BOTH the parent + the inbound child (the trigger discriminator).
1744
- expect(body.tags).toEqual(["#agent/message", "#agent/message/inbound"]);
1745
+ expect(body.tags).toEqual(["agent/message", "agent/message/inbound"]);
1745
1746
  expect(body.content).toBe("Run the morning weave");
1746
1747
  expect(body.metadata.direction).toBe("inbound");
1747
1748
  expect(body.metadata.sender).toBe("runner:morning");
@@ -1789,7 +1790,7 @@ describe("VaultTransport — scheduled-job notes (vault-native store)", () => {
1789
1790
 
1790
1791
  const t = new VaultTransport(baseConfig());
1791
1792
  const jobs = await t.listJobNotes();
1792
- expect(urls[0]).toContain("tag=%23agent%2Fjob");
1793
+ expect(urls[0]).toContain("tag=agent%2Fjob");
1793
1794
  expect(urls[0]).toContain("include_content=true");
1794
1795
  expect(jobs).toHaveLength(2);
1795
1796
  // id = the slug from metadata.jobId; noteId = the vault note id.
@@ -1825,7 +1826,7 @@ describe("VaultTransport — scheduled-job notes (vault-native store)", () => {
1825
1826
  expect(r.id).toBe("Channels/eng/jobs/m");
1826
1827
  const body = JSON.parse(String(calls[0]!.init.body));
1827
1828
  expect(body.path).toBe("Channels/eng/jobs/m");
1828
- expect(body.tags).toEqual(["#agent/job"]);
1829
+ expect(body.tags).toEqual(["agent/job"]);
1829
1830
  expect(body.metadata.enabled).toBe("true");
1830
1831
  expect(body.metadata.jobId).toBe("m"); // slug persisted for stable display
1831
1832
  // CONTRACT: routing key under `metadata.agent` ONLY — no `channel`.
@@ -261,11 +261,11 @@ export class InboundClaimConflictError extends Error {
261
261
  /** Parent tag (NEW, namespaced) — carried LITERALLY on every note WE write; query
262
262
  * this + metadata.channel to see BOTH directions of a channel (the slash children
263
263
  * are namespace, not inheritance). */
264
- const AGENT_MESSAGE_TAG = "#agent/message";
264
+ const AGENT_MESSAGE_TAG = "agent/message";
265
265
  /** Inbound child (NEW) — the vault trigger fires on this exact tag (never matches outbound → no loop). */
266
- const AGENT_MESSAGE_INBOUND_TAG = "#agent/message/inbound";
266
+ const AGENT_MESSAGE_INBOUND_TAG = "agent/message/inbound";
267
267
  /** Outbound child (NEW) — replies carry this; the trigger's exact-match predicate excludes it. */
268
- const AGENT_MESSAGE_OUTBOUND_TAG = "#agent/message/outbound";
268
+ const AGENT_MESSAGE_OUTBOUND_TAG = "agent/message/outbound";
269
269
 
270
270
  /** Metadata key carrying the channel-queue claim status (design 2026-06-18). */
271
271
  const STATUS_META_KEY = "status";
@@ -369,7 +369,7 @@ function buildThreadSummaryBody(t: {
369
369
  * (it always queries the exact leaf tag); it exists for the nice human rollup, per
370
370
  * the design's namespacing decision.
371
371
  */
372
- export const AGENT_ROOT_TAG = "#agent";
372
+ export const AGENT_ROOT_TAG = "agent";
373
373
 
374
374
  /**
375
375
  * Agent-definition tag — a vault-native agent IS a `#agent/definition` note (design
@@ -377,7 +377,7 @@ export const AGENT_ROOT_TAG = "#agent";
377
377
  * METADATA is the config (name, backend, workspace, isolation, the def-vault binding).
378
378
  * The module reads these notes from a def-vault and instantiates each as a live agent.
379
379
  */
380
- export const AGENT_DEFINITION_TAG = "#agent/definition";
380
+ export const AGENT_DEFINITION_TAG = "agent/definition";
381
381
 
382
382
  /**
383
383
  * Scheduled-job tag — the runner's vault-native job store (design
@@ -386,7 +386,7 @@ export const AGENT_DEFINITION_TAG = "#agent/definition";
386
386
  * `#agent/message`. Introduced in Phase 2 as the flat `#agent-job`; moved into the
387
387
  * `#agent/*` namespace (`#agent/job`) by the vault-native-agents work (Phase 4a).
388
388
  */
389
- export const AGENT_JOB_TAG = "#agent/job";
389
+ export const AGENT_JOB_TAG = "agent/job";
390
390
  /** Default path prefix under which job notes are written: `Channels/<ch>/jobs/<id>`. */
391
391
  const JOB_PATH_PREFIX = "Channels";
392
392
 
@@ -413,7 +413,7 @@ const JOB_PATH_PREFIX = "Channels";
413
413
  * The note carries `['#agent/thread']` EXACTLY — NOT a message tag, NOT the inbound
414
414
  * child — so it can never wake a session (no loop).
415
415
  */
416
- export const AGENT_THREAD_TAG = "#agent/thread";
416
+ export const AGENT_THREAD_TAG = "agent/thread";
417
417
  /** Default path prefix under which thread notes are written: `Threads/<ch>/<leaf>`. */
418
418
  const THREAD_PATH_PREFIX = "Threads";
419
419
 
@@ -557,7 +557,7 @@ export const AGENT_VAULT_TRIGGER_TEMPLATE = {
557
557
  name: "channel_inbound_<channel>", // hub substitutes the channel name
558
558
  events: ["created"],
559
559
  when: {
560
- tags: ["#agent/message/inbound"],
560
+ tags: ["agent/message/inbound"],
561
561
  has_metadata: ["agent"],
562
562
  missing_metadata: ["channel_inbound_rendered_at"],
563
563
  },
@@ -581,7 +581,7 @@ export const AGENT_DEF_VAULT_TRIGGER_TEMPLATE = {
581
581
  name: "agent_def_reload",
582
582
  events: ["created", "updated", "deleted"],
583
583
  when: {
584
- tags: ["#agent/definition"],
584
+ tags: ["agent/definition"],
585
585
  },
586
586
  action: {
587
587
  webhook: "<hub-origin>/agent/api/vault/agent-def", // hub fills origin + the auth.bearer
@@ -657,11 +657,11 @@ export class VaultTransport implements Transport {
657
657
  * `decodeURIComponent`'d (parachute-vault `src/routes.ts` handleTags, the
658
658
  * "Routes with tag name" block + `routing.ts` `apiPath.startsWith("/tags")`).
659
659
  * Because the route matches a SINGLE path segment (`[^/]+`, no literal slash)
660
- * and decodes it, the tag name — which contains BOTH `#` and `/`
661
- * (`#agent/message/inbound`) — must be `encodeURIComponent`'d so the `#`
662
- * becomes `%23` and the `/` becomes `%2F`; the route then decodes that back to
663
- * the literal name. A bare `/` in the URL would fail the `[^/]+` match → 404,
664
- * silently dropping the declaration. The PUT body is `{ description?, parent_names? }`.
660
+ * and decodes it, the tag name — which contains a `/`
661
+ * (`agent/message/inbound`) — must be `encodeURIComponent`'d so the `/` becomes
662
+ * `%2F`; the route then decodes that back to the literal name. A bare `/` in the
663
+ * URL would fail the `[^/]+` match → 404, silently dropping the declaration. The
664
+ * PUT body is `{ description?, parent_names? }`.
665
665
  *
666
666
  * Best-effort + non-fatal by contract: every failure is caught and `console.warn`'d,
667
667
  * never thrown — the tag-both write floor is the fallback.
@@ -669,8 +669,8 @@ export class VaultTransport implements Transport {
669
669
  async ensureSchema(): Promise<void> {
670
670
  for (const entry of AGENT_VAULT_TAG_SCHEMA) {
671
671
  try {
672
- // Single-segment, percent-encoded name: `#agent/message/inbound` →
673
- // `%23agent%2Fmessage%2Finbound`. The vault decodes it back to the literal.
672
+ // Single-segment, percent-encoded name: `agent/message/inbound` →
673
+ // `agent%2Fmessage%2Finbound`. The vault decodes it back to the literal.
674
674
  const url = `${this.vaultUrl}/vault/${this.vault}/api/tags/${encodeURIComponent(entry.name)}`;
675
675
  const body: {
676
676
  description?: string;
@@ -1099,7 +1099,7 @@ export class VaultTransport implements Transport {
1099
1099
  * namespace, not query inheritance, so we never key off them here.
1100
1100
  *
1101
1101
  * GET <vaultUrl>/vault/<vault>/api/notes
1102
- * ?tag=%23agent%2Fmessage (the `#` + `/` MUST be percent-encoded)
1102
+ * ?tag=agent%2Fmessage (the `/` MUST be percent-encoded)
1103
1103
  * &include_content=true (we need the bodies)
1104
1104
  * &limit=<n> (default 200)
1105
1105
  *
@@ -1138,7 +1138,7 @@ export class VaultTransport implements Transport {
1138
1138
  // empty transcript).
1139
1139
  const fetchByTag = async (tag: string): Promise<RawNote[]> => {
1140
1140
  const params = new URLSearchParams();
1141
- params.set("tag", tag); // URLSearchParams encodes `#` → `%23`
1141
+ params.set("tag", tag); // URLSearchParams encodes `/` → `%2F`
1142
1142
  params.set("include_content", "true");
1143
1143
  params.set("limit", String(fetchLimit));
1144
1144
  const url = `${this.vaultUrl}/vault/${this.vault}/api/notes?${params.toString()}`;
@@ -1380,7 +1380,7 @@ export class VaultTransport implements Transport {
1380
1380
  // Overfetch (the tag query spans all channels) then keep this channel's items.
1381
1381
  const fetchLimit = Math.min(Math.max(limit * 4, 500), 2000);
1382
1382
  const params = new URLSearchParams();
1383
- params.set("tag", AGENT_MESSAGE_INBOUND_TAG); // → %23agent%2Fmessage%2Finbound
1383
+ params.set("tag", AGENT_MESSAGE_INBOUND_TAG); // → agent%2Fmessage%2Finbound
1384
1384
  params.set("include_content", "true");
1385
1385
  params.set("limit", String(fetchLimit));
1386
1386
  // NEWEST-first at the vault (default order_by is `updated_at`) so a hard cap drops
@@ -1508,7 +1508,7 @@ export class VaultTransport implements Transport {
1508
1508
 
1509
1509
  /**
1510
1510
  * List the scheduled-job notes in THIS channel's vault. Queries by the parent
1511
- * `#agent/job` tag (URLSearchParams encodes `#`→`%23`, `/`→`%2F`) and returns ALL job
1511
+ * `#agent/job` tag (URLSearchParams encodes `/`→`%2F`) and returns ALL job
1512
1512
  * notes in the vault — the CALLER filters by `metadata.channel` (same index-free
1513
1513
  * pattern as loadTranscript; we don't assume a `channel` index exists). Throws
1514
1514
  * on a non-ok vault response so the API surfaces a clear error rather than a
@@ -1517,7 +1517,7 @@ export class VaultTransport implements Transport {
1517
1517
  async listJobNotes(opts?: { limit?: number }): Promise<JobNote[]> {
1518
1518
  const limit = opts?.limit ?? 500;
1519
1519
  const params = new URLSearchParams();
1520
- params.set("tag", AGENT_JOB_TAG); // → %23agent%2Fjob
1520
+ params.set("tag", AGENT_JOB_TAG); // → agent%2Fjob
1521
1521
  params.set("include_content", "true");
1522
1522
  params.set("limit", String(limit));
1523
1523
  const url = `${this.vaultUrl}/vault/${this.vault}/api/notes?${params.toString()}`;