pilotswarm-sdk 0.5.39 → 0.5.40

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 (46) hide show
  1. package/api/index.js +1 -0
  2. package/api/src/api-client.js +46 -0
  3. package/api/src/canvas-live-mirror.js +134 -0
  4. package/api/src/http-api-transport.js +92 -1
  5. package/api/src/protocol.js +4 -0
  6. package/dist/cms-migrations.d.ts.map +1 -1
  7. package/dist/cms-migrations.js +102 -0
  8. package/dist/cms-migrations.js.map +1 -1
  9. package/dist/cms.d.ts +110 -0
  10. package/dist/cms.d.ts.map +1 -1
  11. package/dist/cms.js +157 -0
  12. package/dist/cms.js.map +1 -1
  13. package/dist/facts-store.d.ts.map +1 -1
  14. package/dist/facts-store.js +7 -2
  15. package/dist/facts-store.js.map +1 -1
  16. package/dist/facts-tools.d.ts +7 -0
  17. package/dist/facts-tools.d.ts.map +1 -1
  18. package/dist/facts-tools.js +338 -3
  19. package/dist/facts-tools.js.map +1 -1
  20. package/dist/managed-session.d.ts.map +1 -1
  21. package/dist/managed-session.js +72 -20
  22. package/dist/managed-session.js.map +1 -1
  23. package/dist/management-client.d.ts +42 -0
  24. package/dist/management-client.d.ts.map +1 -1
  25. package/dist/management-client.js +66 -0
  26. package/dist/management-client.js.map +1 -1
  27. package/dist/session-manager.d.ts.map +1 -1
  28. package/dist/session-manager.js +4 -0
  29. package/dist/session-manager.js.map +1 -1
  30. package/dist/session-proxy.d.ts.map +1 -1
  31. package/dist/session-proxy.js +186 -27
  32. package/dist/session-proxy.js.map +1 -1
  33. package/dist/web/generated-op-methods.d.ts +31 -0
  34. package/dist/web/generated-op-methods.d.ts.map +1 -1
  35. package/dist/web/generated-op-methods.js +8 -0
  36. package/dist/web/generated-op-methods.js.map +1 -1
  37. package/dist/web/web-management-client.d.ts +22 -0
  38. package/dist/web/web-management-client.d.ts.map +1 -1
  39. package/dist/web/web-management-client.js +18 -0
  40. package/dist/web/web-management-client.js.map +1 -1
  41. package/package.json +3 -3
  42. package/plugins/default-agents/agents/generic-crawler.agent.md +10 -2
  43. package/plugins/mgmt/agents/facts-manager.agent.md +11 -1
  44. package/plugins/mgmt/agents/pilotswarm.agent.md +2 -2
  45. package/plugins/system/agents/default.agent.md +2 -2
  46. package/plugins/system/skills/html-visuals/SKILL.md +36 -1
@@ -1298,6 +1298,42 @@ artifactStore) {
1298
1298
  return latest;
1299
1299
  }
1300
1300
  let canvasDrawChain = Promise.resolve();
1301
+ // Per-slot tick throttle (the plane makes ticks cheap; a runaway
1302
+ // loop must not flood viewers). Execution-scoped, like the chain.
1303
+ // Keys are `${targetSessionId}:${slot}` — a child ticking its own
1304
+ // canvas and its parent's dashboard are separate budgets.
1305
+ const canvasTickClock = new Map();
1306
+ const CANVAS_TICK_MIN_INTERVAL_MS = 100;
1307
+ /**
1308
+ * Cross-session canvas targeting: ANCESTORS ONLY. A sub-agent may
1309
+ * draw on its parent's (or grandparent's, or the root's) surface —
1310
+ * never a sibling's, a child's, or a stranger's. Worker-trusted,
1311
+ * the same stance as fromArtifact; the walk is the catalog's parent
1312
+ * chain, capped at the spawn-nesting depth.
1313
+ */
1314
+ const resolveCanvasTarget = async (requested) => {
1315
+ const requestedId = String(requested ?? "").trim();
1316
+ if (!requestedId || requestedId === input.sessionId) {
1317
+ return { target: input.sessionId, crossSession: false };
1318
+ }
1319
+ let cursor = input.sessionId;
1320
+ for (let depth = 0; depth < 8 && cursor; depth++) {
1321
+ let row;
1322
+ try {
1323
+ row = await catalog?.getSession?.(cursor);
1324
+ }
1325
+ catch (err) {
1326
+ return { error: `could not verify the session lineage: ${err?.message || String(err)}` };
1327
+ }
1328
+ const parent = row?.parentSessionId ?? null;
1329
+ if (!parent)
1330
+ break;
1331
+ if (parent === requestedId)
1332
+ return { target: requestedId, crossSession: true };
1333
+ cursor = parent;
1334
+ }
1335
+ return { error: `session_id ${requestedId} is not an ancestor of this session — canvas tools may target only your parent chain (parent, grandparent, root)` };
1336
+ };
1301
1337
  const controlToolBridge = {
1302
1338
  /**
1303
1339
  * Send a message to a session AS ITS USER.
@@ -2078,8 +2114,11 @@ artifactStore) {
2078
2114
  if (rawName !== undefined && rawName.length > 60) {
2079
2115
  return { error: "name must be 60 characters or fewer" };
2080
2116
  }
2117
+ const resolved = await resolveCanvasTarget(args.session_id);
2118
+ if ("error" in resolved)
2119
+ return { error: resolved.error };
2120
+ const { target, crossSession } = resolved;
2081
2121
  const run = canvasDrawChain.then(async () => {
2082
- const rev = (await latestCanvasRev(catalog, input.sessionId, slot)) + 1;
2083
2122
  // Source resolution. fromArtifact pulls bytes store-side —
2084
2123
  // the same trust stance as read_artifact/write_artifact's
2085
2124
  // cross-session paths (the artifact layer is worker-trusted);
@@ -2143,7 +2182,22 @@ artifactStore) {
2143
2182
  }
2144
2183
  const app = canvasAppCard(extraction.manifest);
2145
2184
  const note = args.note ? String(args.note) : undefined;
2146
- await artifactStore.uploadArtifact(input.sessionId, canvasArtifactFilename(slot), html, "text/html",
2185
+ // Atomic rev mint (multi-writer safe), AFTER every
2186
+ // validation refusal above: the mint writes latest_rev
2187
+ // into the 0045 cache, and a rev minted for a draw
2188
+ // that then failed validation would unlock ticks on an
2189
+ // empty slot (latestCanvasRev is table-first). The
2190
+ // seed from the table/event read floors legacy
2191
+ // sessions whose cache row never landed. Residual
2192
+ // window: an upload/record failure AFTER the mint
2193
+ // burns a phantom rev — infra-failure only, and the
2194
+ // next successful draw overwrites it; read_canvas
2195
+ // stays honest because it is log-first.
2196
+ const seedRev = await latestCanvasRev(catalog, target, slot);
2197
+ const rev = typeof catalog.mintCanvasRev === "function"
2198
+ ? await catalog.mintCanvasRev(target, slot, seedRev)
2199
+ : seedRev + 1;
2200
+ await artifactStore.uploadArtifact(target, canvasArtifactFilename(slot), html, "text/html",
2147
2201
  // Pinned on EVERY draw, not just the first: uploads
2148
2202
  // replace artifact metadata wholesale, so a pin set
2149
2203
  // once at rev 1 was silently erased by rev 2.
@@ -2154,7 +2208,7 @@ artifactStore) {
2154
2208
  // The response contract rides the event so every
2155
2209
  // client learns it exactly where it learns the rev —
2156
2210
  // live push and cold snapshot alike, no extra fetch.
2157
- await catalog.recordEvents(input.sessionId, [{
2211
+ await catalog.recordEvents(target, [{
2158
2212
  eventType: "session.canvas_updated",
2159
2213
  data: {
2160
2214
  rev,
@@ -2164,6 +2218,10 @@ artifactStore) {
2164
2218
  ...(note ? { note } : {}),
2165
2219
  ...(effectiveContract ? { responseContract: effectiveContract } : {}),
2166
2220
  ...(source ? { source } : {}),
2221
+ // Attribution: who actually drew this revision.
2222
+ // Only present on cross-session draws, so the
2223
+ // portal can badge "drawn by sub-agent X".
2224
+ ...(crossSession ? { by: input.sessionId } : {}),
2167
2225
  },
2168
2226
  }], workerNodeId);
2169
2227
  // The per-slot cache (migration 0045). Non-fatal on
@@ -2172,9 +2230,21 @@ artifactStore) {
2172
2230
  // missing — failing the draw over a cache write would
2173
2231
  // invert the dependency.
2174
2232
  try {
2175
- await catalog.upsertSessionCanvas?.(input.sessionId, slot, rawName ?? null, rev, sizeBytes);
2233
+ await catalog.upsertSessionCanvas?.(target, slot, rawName ?? null, rev, sizeBytes);
2176
2234
  }
2177
2235
  catch { /* self-heals on the next draw */ }
2236
+ // The data plane (migration 0047): doc pointer for
2237
+ // live viewers + RESET of the data mirror — the new
2238
+ // page starts from its own initial state, never the
2239
+ // old page's stale ticks. Non-fatal: the plane is the
2240
+ // acceleration path, the event above is the truth.
2241
+ try {
2242
+ if (await catalog.canvasLiveAvailable?.()) {
2243
+ const sha = createHash("sha256").update(html, "utf8").digest("hex");
2244
+ await catalog.upsertCanvasLiveDoc?.(target, slot, { rev, sha }, input.sessionId);
2245
+ }
2246
+ }
2247
+ catch { /* live viewers resync from events */ }
2178
2248
  return {
2179
2249
  rev,
2180
2250
  slot,
@@ -2195,25 +2265,87 @@ artifactStore) {
2195
2265
  const slot = normalizeCanvasSlot(args.slot);
2196
2266
  if (slot === null)
2197
2267
  return { error: "slot must be an integer 1-5" };
2268
+ const resolved = await resolveCanvasTarget(args.session_id);
2269
+ if ("error" in resolved)
2270
+ return { error: resolved.error };
2271
+ const { target, crossSession } = resolved;
2272
+ const isPatch = args.patch !== undefined;
2273
+ const body = isPatch ? args.patch : args.data;
2274
+ // Rate limit at the source: the plane makes ticks cheap
2275
+ // enough that a runaway loop could flood viewers. Applies
2276
+ // per (target, slot) within this execution.
2277
+ const now = Date.now();
2278
+ const tickKey = `${target}:${slot}`;
2279
+ const lastTick = canvasTickClock.get(tickKey) || 0;
2280
+ if (now - lastTick < CANVAS_TICK_MIN_INTERVAL_MS) {
2281
+ return { error: `ticking slot ${slot} faster than ${CANVAS_TICK_MIN_INTERVAL_MS} ms apart — aggregate your changes into fewer, larger ticks` };
2282
+ }
2283
+ canvasTickClock.set(tickKey, now);
2198
2284
  const run = canvasDrawChain.then(async () => {
2199
- if ((await latestCanvasRev(catalog, input.sessionId, slot)) === 0) {
2285
+ if ((await latestCanvasRev(catalog, target, slot)) === 0) {
2200
2286
  return { error: `no canvas has been drawn in slot ${slot} — draw_canvas first; ticks patch an existing page` };
2201
2287
  }
2202
- const dataRev = (await latestCanvasDataRev(catalog, input.sessionId, slot)) + 1;
2203
- const sizeBytes = Buffer.byteLength(JSON.stringify(args.data), "utf8");
2204
- // The payload rides the durable event — it IS the
2205
- // replay source for cold loads; no artifact write.
2206
- await catalog.recordEvents(input.sessionId, [{
2207
- eventType: "session.canvas_data",
2208
- data: {
2209
- dataRev,
2210
- slot,
2211
- sizeBytes,
2212
- payload: args.data,
2213
- ...(args.note ? { note: String(args.note) } : {}),
2214
- },
2215
- }], workerNodeId);
2216
- return { dataRev, slot, sizeBytes };
2288
+ const planeAvailable = await catalog.canvasLiveAvailable?.().catch(() => false);
2289
+ // The plane is where merges live. Without it (older
2290
+ // deployment), a patch has no current state to merge
2291
+ // against — refuse with the actionable alternative
2292
+ // instead of guessing.
2293
+ if (isPatch && !planeAvailable) {
2294
+ return { error: "this deployment predates the canvas data plane — send data (the whole state) instead of patch" };
2295
+ }
2296
+ // Whole state after this write, whoever computes it:
2297
+ // the plane merges patches server-side; a plain PUT is
2298
+ // its own whole state.
2299
+ let mergedPayload = (body ?? {});
2300
+ let planeSeq;
2301
+ if (planeAvailable) {
2302
+ const live = await catalog.upsertCanvasLiveTick(target, slot, isPatch ? { patch: body } : { data: body }, input.sessionId, 32_768);
2303
+ if (live?.refused) {
2304
+ return {
2305
+ error: `the merged canvas state would exceed 32768 bytes`
2306
+ + (live.currentSizeBytes != null ? ` (currently ${live.currentSizeBytes} bytes)` : "")
2307
+ + `. Aggregate the data, remove stale keys (patch with null deletes), or redraw if the shape truly grew.`,
2308
+ };
2309
+ }
2310
+ mergedPayload = live.payload ?? mergedPayload;
2311
+ planeSeq = live.seq;
2312
+ }
2313
+ const sizeBytes = Buffer.byteLength(JSON.stringify(mergedPayload), "utf8");
2314
+ // Dual-write phase: the durable event carries the
2315
+ // MERGED whole state so pre-plane readers (older
2316
+ // portals, cold loads) stay complete. Flipping
2317
+ // PILOTSWARM_CANVAS_DURABLE_TICKS=0 ends this — only
2318
+ // after every reader in the environment prefers the
2319
+ // plane, and the completion checkpoint ships with
2320
+ // that flip.
2321
+ let dataRev;
2322
+ // The flag only silences durable ticks where the
2323
+ // plane actually took the write. Plane absent =
2324
+ // today's path stays, whatever the flag says —
2325
+ // otherwise a PUT would be written NOWHERE and still
2326
+ // report success.
2327
+ if (!planeAvailable || process.env.PILOTSWARM_CANVAS_DURABLE_TICKS !== "0") {
2328
+ dataRev = (await latestCanvasDataRev(catalog, target, slot)) + 1;
2329
+ await catalog.recordEvents(target, [{
2330
+ eventType: "session.canvas_data",
2331
+ data: {
2332
+ dataRev,
2333
+ slot,
2334
+ sizeBytes,
2335
+ payload: mergedPayload,
2336
+ ...(isPatch ? { patched: true } : {}),
2337
+ ...(crossSession ? { by: input.sessionId } : {}),
2338
+ ...(args.note ? { note: String(args.note) } : {}),
2339
+ },
2340
+ }], workerNodeId);
2341
+ }
2342
+ return {
2343
+ ...(dataRev !== undefined ? { dataRev } : {}),
2344
+ ...(planeSeq !== undefined ? { seq: planeSeq } : {}),
2345
+ slot,
2346
+ sizeBytes,
2347
+ mode: isPatch ? "patch" : "data",
2348
+ };
2217
2349
  }).catch((err) => ({ error: err?.message || String(err) }));
2218
2350
  canvasDrawChain = run.then(() => undefined, () => undefined);
2219
2351
  return run;
@@ -2228,13 +2360,17 @@ artifactStore) {
2228
2360
  const slot = normalizeCanvasSlot(args.slot);
2229
2361
  if (slot === null)
2230
2362
  return { error: "slot must be an integer 1-5" };
2231
- const rev = await latestCanvasRev(catalog, input.sessionId, slot);
2363
+ const resolved = await resolveCanvasTarget(args.session_id);
2364
+ if ("error" in resolved)
2365
+ return { error: resolved.error };
2366
+ const { target, crossSession } = resolved;
2367
+ const rev = await latestCanvasRev(catalog, target, slot);
2232
2368
  if (rev === 0) {
2233
2369
  return { error: `nothing has been drawn in slot ${slot} — draw_canvas first; show_canvas only presents an existing canvas` };
2234
2370
  }
2235
- await catalog.recordEvents(input.sessionId, [{
2371
+ await catalog.recordEvents(target, [{
2236
2372
  eventType: "session.canvas_presented",
2237
- data: { slot, rev },
2373
+ data: { slot, rev, ...(crossSession ? { by: input.sessionId } : {}) },
2238
2374
  }], workerNodeId);
2239
2375
  return { presented: true, slot, rev };
2240
2376
  },
@@ -2246,6 +2382,27 @@ artifactStore) {
2246
2382
  const slot = normalizeCanvasSlot(args.slot);
2247
2383
  if (slot === null)
2248
2384
  return { error: "slot must be an integer 1-5" };
2385
+ const resolved = await resolveCanvasTarget(args.session_id);
2386
+ if ("error" in resolved)
2387
+ return { error: resolved.error };
2388
+ const { target } = resolved;
2389
+ // "What is the page showing right now?" — the plane's
2390
+ // last-value row, one PK read. Opt-in (the payload can be
2391
+ // 32 KB of model context) and independent of paging.
2392
+ const liveState = async () => {
2393
+ if (!args.includeData)
2394
+ return {};
2395
+ try {
2396
+ if (!(await catalog.canvasLiveAvailable?.()))
2397
+ return {};
2398
+ const rows = await catalog.getCanvasLive?.(target);
2399
+ const hit = (rows || []).find((r) => Number(r.slot) === slot);
2400
+ return hit ? { live: { seq: hit.seq, data: hit.payload, updatedBy: hit.updatedBy, updatedAt: hit.updatedAt } } : {};
2401
+ }
2402
+ catch {
2403
+ return {};
2404
+ }
2405
+ };
2249
2406
  try {
2250
2407
  // Log FIRST: the event log is the single authority on
2251
2408
  // whether a canvas exists. Orphan bytes from a crashed
@@ -2261,10 +2418,10 @@ artifactStore) {
2261
2418
  // ARMED contract from the latest draw event — the
2262
2419
  // pair an inheriting agent needs to interpret
2263
2420
  // canvas-action messages and author ticks.
2264
- const latest = await latestCanvasEventData(catalog, input.sessionId, slot);
2421
+ const latest = await latestCanvasEventData(catalog, target, slot);
2265
2422
  if (latest.rev === 0)
2266
2423
  return { exists: false };
2267
- const docText = await artifactStore.downloadArtifactText(input.sessionId, canvasArtifactFilename(slot));
2424
+ const docText = await artifactStore.downloadArtifactText(target, canvasArtifactFilename(slot));
2268
2425
  const extraction = extractCanvasAppManifest(docText);
2269
2426
  const card = canvasAppCard(extraction.manifest);
2270
2427
  // Events are writable via send_session_event with no
@@ -2279,12 +2436,13 @@ artifactStore) {
2279
2436
  ...(card ? { app: card } : {}),
2280
2437
  ...(armed.contract ? { responseContract: armed.contract } : {}),
2281
2438
  ...(extraction.error ? { manifestError: extraction.error } : {}),
2439
+ ...(await liveState()),
2282
2440
  };
2283
2441
  }
2284
- const rev = await latestCanvasRev(catalog, input.sessionId, slot);
2442
+ const rev = await latestCanvasRev(catalog, target, slot);
2285
2443
  if (rev === 0)
2286
2444
  return { exists: false };
2287
- const text = await artifactStore.downloadArtifactText(input.sessionId, canvasArtifactFilename(slot));
2445
+ const text = await artifactStore.downloadArtifactText(target, canvasArtifactFilename(slot));
2288
2446
  const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
2289
2447
  const maxChars = Math.min(262_144, Math.max(1, Math.floor(Number(args.maxBytes) || 65_536)));
2290
2448
  const content = text.slice(offset, offset + maxChars);
@@ -2298,6 +2456,7 @@ artifactStore) {
2298
2456
  offset,
2299
2457
  content,
2300
2458
  truncated: offset + content.length < text.length,
2459
+ ...(await liveState()),
2301
2460
  };
2302
2461
  }
2303
2462
  catch (err) {