@computesdk/loops 0.1.4 → 0.1.6

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 (71) hide show
  1. package/API.md +15 -1
  2. package/README.md +14 -0
  3. package/client.ts +2 -8
  4. package/dist/assets/{abnfDiagram-VCTEODGH-rmJwkbxW.js → abnfDiagram-VCTEODGH-BmAHMdfH.js} +1 -1
  5. package/dist/assets/{arc-aSBi73PQ.js → arc-D_2zELzk.js} +1 -1
  6. package/dist/assets/{architectureDiagram-5GKGNRK7-Cq32yybi.js → architectureDiagram-5GKGNRK7-DbtMlGE1.js} +1 -1
  7. package/dist/assets/{blockDiagram-I7D4REHJ-C-PFhPqQ.js → blockDiagram-I7D4REHJ-oeouAQDw.js} +1 -1
  8. package/dist/assets/{c4Diagram-7LVT6UL2-B4jKtTT9.js → c4Diagram-7LVT6UL2-BRo4V2Z7.js} +1 -1
  9. package/dist/assets/channel-GK1ExE_g.js +1 -0
  10. package/dist/assets/{chunk-2Q5K7J3B-CeU3QCre.js → chunk-2Q5K7J3B-1yTKgMZ4.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-BThX-upu.js → chunk-5VM5RSS4-BsMua1fA.js} +1 -1
  12. package/dist/assets/{chunk-F27PBJKO-BkZOip-K.js → chunk-F27PBJKO-BFx0TMpb.js} +1 -1
  13. package/dist/assets/{chunk-IMKFNOWR-D1qNuGK_.js → chunk-IMKFNOWR-D8UfXi3T.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-BEeC9bHr.js → chunk-JWPE2WC7-bBfJHxEJ.js} +1 -1
  15. package/dist/assets/{chunk-POPQ4Y6H-eA1aBSf8.js → chunk-POPQ4Y6H-CLnUbfSj.js} +1 -1
  16. package/dist/assets/{chunk-SVP7TREG-CzlTp8tj.js → chunk-SVP7TREG-uWuiRTo9.js} +1 -1
  17. package/dist/assets/{chunk-TICWLB2K-iCYUhmUO.js → chunk-TICWLB2K-DV5DzCt1.js} +1 -1
  18. package/dist/assets/{chunk-XXDRQBXY-j5Ron3J8.js → chunk-XXDRQBXY-DflGukDx.js} +1 -1
  19. package/dist/assets/classDiagram-ZZMXUADV-veE8mBSk.js +1 -0
  20. package/dist/assets/classDiagram-v2-VYDZK3BY-veE8mBSk.js +1 -0
  21. package/dist/assets/{cose-bilkent-JH36ORCC-w0OitGtv.js → cose-bilkent-JH36ORCC-CXrAPLcM.js} +1 -1
  22. package/dist/assets/{cynefin-OW5HDTMX-OLlhwFhm.js → cynefin-OW5HDTMX-BZAor1Fd.js} +1 -1
  23. package/dist/assets/{cynefinDiagram-5FMLGOSQ-DAIF0opu.js → cynefinDiagram-5FMLGOSQ-2mtwTNrl.js} +1 -1
  24. package/dist/assets/{dagre-GXQ25YYZ-H_tRA8C3.js → dagre-GXQ25YYZ-BpwBh8sN.js} +1 -1
  25. package/dist/assets/{diagram-S7CK7UJ4-BO3eHMoV.js → diagram-S7CK7UJ4-BtWd6QOl.js} +1 -1
  26. package/dist/assets/{diagram-UQ7AKVKN-CH7FayQf.js → diagram-UQ7AKVKN-918gle9R.js} +1 -1
  27. package/dist/assets/{diagram-VSXAHHWV-Chh0kFtU.js → diagram-VSXAHHWV-BrjIR_Ik.js} +1 -1
  28. package/dist/assets/{diagram-VX7I27RA-CXgNEbN7.js → diagram-VX7I27RA-C28OpNTD.js} +1 -1
  29. package/dist/assets/{diagram-Z3DM3KII-COvioQLg.js → diagram-Z3DM3KII-m__eZu-M.js} +1 -1
  30. package/dist/assets/{ebnfDiagram-PWID7BFC-IM5LRIiK.js → ebnfDiagram-PWID7BFC-DX1w-kGZ.js} +1 -1
  31. package/dist/assets/{erDiagram-RLTQ6QDP-DwLblHb1.js → erDiagram-RLTQ6QDP-B7Ixmqav.js} +1 -1
  32. package/dist/assets/{flowDiagram-HODETNUW-BqT9Pj2j.js → flowDiagram-HODETNUW-Bom5HiIz.js} +1 -1
  33. package/dist/assets/{ganttDiagram-EL5Y4UJY-RjLtOHIE.js → ganttDiagram-EL5Y4UJY-oGHhHPme.js} +1 -1
  34. package/dist/assets/{gitGraphDiagram-WWUBYQGX-Dr80A4cy.js → gitGraphDiagram-WWUBYQGX-BpdyfMmU.js} +1 -1
  35. package/dist/assets/{highlighted-body-JLMTFACH-BXGUXFR6.js → highlighted-body-JLMTFACH-DqYsmdT7.js} +1 -1
  36. package/dist/assets/index-D3WPco0Q.css +1 -0
  37. package/dist/assets/index-XP9FiM_q.js +902 -0
  38. package/dist/assets/{infoDiagram-27XIBGKW-DkRLZyNK.js → infoDiagram-27XIBGKW-CwHXRfO4.js} +1 -1
  39. package/dist/assets/{ishikawaDiagram-5VMMS53U-DprIz3OL.js → ishikawaDiagram-5VMMS53U-CDMEI2BQ.js} +1 -1
  40. package/dist/assets/{journeyDiagram-3NMN7TZE-Cyuw80dG.js → journeyDiagram-3NMN7TZE-Ba4NonhC.js} +1 -1
  41. package/dist/assets/{kanban-definition-UXKFOSKX-Bg_5N9_d.js → kanban-definition-UXKFOSKX-B7X_m14V.js} +1 -1
  42. package/dist/assets/{layout-r28aTTlm.js → layout-CYu8n8qm.js} +1 -1
  43. package/dist/assets/{linear-BKwkVe0c.js → linear-BBLkaaSf.js} +1 -1
  44. package/dist/assets/{mindmap-definition-YA3MSWOX-D-tP2Z-N.js → mindmap-definition-YA3MSWOX-BEACV2JJ.js} +1 -1
  45. package/dist/assets/{pegDiagram-XKGWAZYB-CFlfyIN4.js → pegDiagram-XKGWAZYB-BpkdZ-6G.js} +1 -1
  46. package/dist/assets/{pieDiagram-E7YTZNPT-CNKQ5goz.js → pieDiagram-E7YTZNPT-_DiRoL0F.js} +1 -1
  47. package/dist/assets/{quadrantDiagram-AXDQQJYC-0BQiaJkL.js → quadrantDiagram-AXDQQJYC-CG5zvdga.js} +1 -1
  48. package/dist/assets/{railroadDiagram-O6MQD6OU-D-abldoP.js → railroadDiagram-O6MQD6OU-G4hMBXmt.js} +1 -1
  49. package/dist/assets/{requirementDiagram-BXWQKSXE-CVFpCeZa.js → requirementDiagram-BXWQKSXE-BkZ5guig.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-P5KCCOFB-B-ShpZg7.js → sankeyDiagram-P5KCCOFB-C5PzZw7u.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-WJ2MYXX4-Dds0Csl1.js → sequenceDiagram-WJ2MYXX4-UAlhcqTK.js} +1 -1
  52. package/dist/assets/{sizeCapture-INFHLROL-DnmQEpM8.js → sizeCapture-INFHLROL-DNRbEYss.js} +1 -1
  53. package/dist/assets/{stateDiagram-D77RDMKH-DxjhM7Uf.js → stateDiagram-D77RDMKH-DPCeQvnD.js} +1 -1
  54. package/dist/assets/stateDiagram-v2-MP3YSRHH-CtZUrpem.js +1 -0
  55. package/dist/assets/{swimlanes-42K2YHIH-CYyjkOVO.js → swimlanes-42K2YHIH-Dr76qlly.js} +1 -1
  56. package/dist/assets/swimlanesDiagram-VR7AAH4N-CWvCwFtT.js +8 -0
  57. package/dist/assets/{timeline-definition-24CTP7MA-DxbZ-ELG.js → timeline-definition-24CTP7MA-jelGqxUz.js} +1 -1
  58. package/dist/assets/{vennDiagram-4TSXK5OY-CriTGGlx.js → vennDiagram-4TSXK5OY-CfIVRv2v.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-VM6X3IG4-BfrfqQx4.js → wardleyDiagram-VM6X3IG4-BM5Zu9IN.js} +1 -1
  60. package/dist/assets/{xychartDiagram-S5SC5T6Z-Bcg1QYkE.js → xychartDiagram-S5SC5T6Z-DYG0fGby.js} +1 -1
  61. package/dist/index.html +2 -2
  62. package/dist/server.mjs +175 -47
  63. package/package.json +3 -3
  64. package/src/shared/types.ts +19 -2
  65. package/dist/assets/channel-BWhkGVIf.js +0 -1
  66. package/dist/assets/classDiagram-ZZMXUADV-C0ZfnZDP.js +0 -1
  67. package/dist/assets/classDiagram-v2-VYDZK3BY-C0ZfnZDP.js +0 -1
  68. package/dist/assets/index-Bejffjbf.js +0 -902
  69. package/dist/assets/index-CO9BshXI.css +0 -1
  70. package/dist/assets/stateDiagram-v2-MP3YSRHH-B3k-YTyy.js +0 -1
  71. package/dist/assets/swimlanesDiagram-VR7AAH4N-GJ1L3IlY.js +0 -8
package/dist/server.mjs CHANGED
@@ -2833,6 +2833,11 @@ function stateFromEntries(entries, id) {
2833
2833
  const newer = custom.filter((e) => e.customType === "loops-round" && e.data?.run === id && Number(e.data?.at ?? 0) > snapAt);
2834
2834
  const latest = newer.at(-1)?.data?.best;
2835
2835
  const winner = latest?.label && latest.round != null ? { ...base.best ?? {}, ...Object.fromEntries(Object.entries(latest).filter(([, v]) => v != null)) } : null;
2836
+ if (winner) {
2837
+ const sameWinner = latest.label === base.best?.label && latest.round === base.best?.round && latest.commit === base.best?.commit;
2838
+ if (Object.hasOwn(latest, "applied")) winner.applied = latest.applied;
2839
+ else if (!sameWinner) delete winner.applied;
2840
+ }
2836
2841
  return {
2837
2842
  ...base,
2838
2843
  ...winner ? { best: winner } : {},
@@ -2918,6 +2923,7 @@ function buildRun({ cwd, id, entries, live }) {
2918
2923
  model: pretty(a.model),
2919
2924
  style: a.style,
2920
2925
  status: a.status ?? "done",
2926
+ cut: a.cut,
2921
2927
  score,
2922
2928
  kept,
2923
2929
  humanPick: !!(kept && (r.human || chosen.round === r.n && !!chosen.label && chosen.label !== r.pick)),
@@ -2951,6 +2957,7 @@ function buildRun({ cwd, id, entries, live }) {
2951
2957
  judgeCost: r.judgeCost ?? void 0,
2952
2958
  startedAt: r.startedAt ?? void 0,
2953
2959
  endedAt: r.endedAt ?? void 0,
2960
+ status: r.status,
2954
2961
  inconclusive: !!r.inconclusive,
2955
2962
  unseen: r.unseen ?? [],
2956
2963
  ...r.escaped ? { escaped: r.escaped } : {}
@@ -2974,8 +2981,19 @@ function buildRun({ cwd, id, entries, live }) {
2974
2981
  }));
2975
2982
  const round = { n, from: { round: n - 1, label: "CURRENT" }, attempts, inProgress: true, judging: /judg/.test(live.phase ?? ""), why: void 0 };
2976
2983
  const at = rounds.findIndex((r) => r.n === n);
2977
- if (at >= 0) rounds[at] = { ...rounds[at], inProgress: true, judging: round.judging };
2978
- else rounds.push(round);
2984
+ if (at >= 0) {
2985
+ const saved = rounds[at].attempts;
2986
+ const incoming = new Map(attempts.map((a) => [a.name, a]));
2987
+ rounds[at] = {
2988
+ ...rounds[at],
2989
+ inProgress: true,
2990
+ judging: round.judging,
2991
+ attempts: [
2992
+ ...saved.map((a) => incoming.has(a.name) ? { ...a, ...incoming.get(a.name), hasSession: a.hasSession || incoming.get(a.name).hasSession } : a),
2993
+ ...attempts.filter((a) => !saved.some((old) => old.name === a.name))
2994
+ ]
2995
+ };
2996
+ } else rounds.push(round);
2979
2997
  }
2980
2998
  const bestRound = state?.best?.round ?? roundEntries.filter((r) => r.pick && r.pick !== "CURRENT").at(-1)?.n ?? 0;
2981
2999
  const bestEntry = roundEntries.filter((r) => r.n === bestRound).at(-1);
@@ -2987,12 +3005,27 @@ function buildRun({ cwd, id, entries, live }) {
2987
3005
  const firstT = Number(roundEntries[0]?.startedAt ?? Date.parse(head.at ?? "") ?? Date.now());
2988
3006
  log.push({ t: Number.isFinite(firstT) ? firstT : Date.now(), kind: "start", text: `Started ${head.goal ?? state?.goal ?? ""}`.trim() });
2989
3007
  for (const r of roundEntries) {
2990
- log.push({ t: Number(r.endedAt ?? r.at ?? Date.now()), kind: "judged", text: "", round: r.n, pick: r.pick ?? void 0, why: r.why ?? void 0, scores: r.scores ?? void 0 });
3008
+ const judged = (!r.status || r.status === "done") && !!r.pick && !r.inconclusive && !r.unseen?.length;
3009
+ if (judged) log.push({ t: Number(r.endedAt ?? r.at ?? Date.now()), kind: "judged", text: "", round: r.n, pick: r.pick, why: r.why ?? void 0, scores: r.scores ?? void 0 });
3010
+ else if (r.status !== "running") log.push({
3011
+ t: Number(r.endedAt ?? r.at ?? Date.now()),
3012
+ kind: "round",
3013
+ round: r.n,
3014
+ text: `Round ${r.n}: ${r.status === "aborted" ? "stopped before comparison completed" : r.status === "error" ? "failed before comparison completed" : "no completed comparison"}.`
3015
+ });
2991
3016
  }
2992
3017
  const wrote = state?.status ?? head.status ?? "done";
2993
3018
  const stalled = !alive && wrote === "running";
2994
3019
  const status = alive ? "running" : stalled ? "stalled" : wrote;
2995
- if (!alive && (wrote === "done" || wrote === "aborted") && roundEntries.length) log.push({ t: Number(roundEntries.at(-1)?.endedAt ?? Date.now()), kind: "done", text: `finished after ${roundEntries.length} round(s)` });
3020
+ if (!alive && ["done", "aborted", "error"].includes(wrote)) {
3021
+ const reason = state?.stopReason ?? state?.error;
3022
+ const compared = log.filter((e) => e.kind === "judged").length;
3023
+ log.push({
3024
+ t: Number(roundEntries.at(-1)?.endedAt ?? Date.now()),
3025
+ kind: "done",
3026
+ text: wrote === "aborted" ? `Stopped; worktrees kept.${reason ? ` ${reason}` : ""}` : wrote === "error" ? `Failed: ${reason || "run failed before a usable result"}.` : compared ? `finished after ${compared} compared round(s)` : "Ended with no completed comparison."
3027
+ });
3028
+ }
2996
3029
  const tokens = roundEntries.reduce((sum, r) => sum + (r.attempts ?? []).reduce((s, a) => s + (a.tokens ?? 0), 0) + (r.judgeTokens ?? 0), 0) + (live ? (live.attempts ?? []).reduce((s, a) => s + (a.tokens ?? 0), 0) : 0);
2997
3030
  return {
2998
3031
  id,
@@ -3004,10 +3037,17 @@ function buildRun({ cwd, id, entries, live }) {
3004
3037
  goal: state?.goal ?? head.goal ?? "",
3005
3038
  status: status === "aborted" ? "done" : status,
3006
3039
  alive,
3007
- stopReason: stalled ? "no signal from the round \u2014 the process went away" : status === "aborted" ? "aborted" : void 0,
3040
+ stopReason: stalled ? "no signal from the round \u2014 the process went away" : status === "aborted" ? "aborted" : state?.stopReason ?? state?.error,
3008
3041
  rules: rules.map((text) => ({ text, source: "config" })),
3009
3042
  rounds,
3010
- best: { round: bestRound, label: bestLabel ?? "base", score: bestScore, shot: bestLabel && shots.length ? shotUrl(id, bestLabel, shots[0]) : void 0, byHuman: !!(bestEntry && bestLabel && bestEntry.pick && bestEntry.pick !== bestLabel) || !!state?.best?.human },
3043
+ best: {
3044
+ round: bestRound,
3045
+ label: bestLabel ?? "base",
3046
+ score: bestScore,
3047
+ shot: bestLabel && shots.length ? shotUrl(id, bestLabel, shots[0]) : void 0,
3048
+ byHuman: !!(bestEntry && bestLabel && bestEntry.pick && bestEntry.pick !== bestLabel) || !!state?.best?.human,
3049
+ ...typeof state?.best?.applied === "string" && state.best.applied.trim() ? { applied: state.best.applied } : {}
3050
+ },
3011
3051
  scores,
3012
3052
  maxRounds: Math.max(rounds.length, 1),
3013
3053
  tokens,
@@ -3044,7 +3084,15 @@ function summarize(cwd, entries) {
3044
3084
  lastT = t;
3045
3085
  const bestRound = state?.best?.round ?? rounds.filter((r) => r.pick && r.pick !== "CURRENT").at(-1)?.n;
3046
3086
  const bestEntry = rounds.find((r) => r.n === bestRound);
3047
- latest = { name: goalName(state?.goal ?? head.goal ?? id), status: stalled ? "stalled" : status, kind: head.kind ?? state?.kind ?? "visual", score: state?.best?.score ?? (bestEntry?.pick ? bestEntry.scores?.[bestEntry.pick] : void 0), unit: state?.metric?.unit ?? rounds.at(-1)?.metric?.unit ?? "", rounds: rounds.length };
3087
+ const attempts = rounds.flatMap((r) => r.attempts ?? []);
3088
+ const failures = /* @__PURE__ */ new Map();
3089
+ for (const a of attempts) {
3090
+ const why = a.status === "budget" ? "token budget" : a.status === "timeout" ? "timed out" : String(a.status).startsWith("error") ? "error" : a.status === "aborted" ? "stopped" : null;
3091
+ if (why) failures.set(why, (failures.get(why) ?? 0) + 1);
3092
+ }
3093
+ const details = [...failures].map(([why, n]) => `${n} ${why}`).join(", ");
3094
+ const outcome = alive ? void 0 : status === "aborted" ? "Stopped \u2014 worktrees kept" : rounds.at(-1)?.inconclusive || rounds.at(-1)?.unseen?.length ? "Inconclusive \u2014 judging incomplete" : attempts.length && attempts.every((a) => !["done", "unchanged", "no-change"].includes(a.status)) ? `No candidates completed${details ? ` \xB7 ${details}` : ""}` : status === "error" ? `Failed: ${state?.stopReason ?? state?.error ?? "run failed"}` : !rounds.length ? "No usable result \u2014 no rounds completed" : void 0;
3095
+ latest = { name: goalName(state?.goal ?? head.goal ?? id), status: stalled ? "stalled" : status, kind: head.kind ?? state?.kind ?? "visual", score: state?.best?.score ?? (bestEntry?.pick ? bestEntry.scores?.[bestEntry.pick] : void 0), unit: state?.metric?.unit ?? rounds.at(-1)?.metric?.unit ?? "", rounds: rounds.length, ...outcome ? { outcome } : {} };
3048
3096
  }
3049
3097
  }
3050
3098
  return { loops: ids.length, running, open, question, lastT, latest };
@@ -3266,6 +3314,12 @@ var Chat = class {
3266
3314
  }
3267
3315
  onEvent(event) {
3268
3316
  const type = event.type;
3317
+ if (type === "extension_error") {
3318
+ this.notices.push({ t: Date.now(), level: "error", message: String(event.error ?? event.message ?? "Extension command failed") });
3319
+ this.notices = this.notices.slice(-20);
3320
+ this.broadcast(true);
3321
+ return;
3322
+ }
3269
3323
  if (type === "message_start" && event.message?.role === "assistant") {
3270
3324
  this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: "", text: "", tools: [] };
3271
3325
  this.broadcast();
@@ -3318,11 +3372,18 @@ var Chat = class {
3318
3372
  this.touch();
3319
3373
  }
3320
3374
  async prompt(text, behavior) {
3321
- if (!this.live) this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: text, text: "", tools: [] };
3375
+ const previous = this.live;
3376
+ if (!text.startsWith("/") && !this.live) this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: text, text: "", tools: [] };
3322
3377
  this.broadcast(true);
3323
- const r = await this.pi.prompt(text, behavior);
3324
- setTimeout(() => void this.refresh(), 400);
3325
- return r?.disposition ?? "started";
3378
+ try {
3379
+ const r = await this.pi.prompt(text, behavior);
3380
+ setTimeout(() => void this.refresh(), 400);
3381
+ return r?.disposition ?? "started";
3382
+ } catch (error) {
3383
+ if (!previous && this.live?.user === text) this.live = null;
3384
+ this.broadcast(true);
3385
+ throw error;
3386
+ }
3326
3387
  }
3327
3388
  /** The child's own view of "am I busy": a turn that settled with no event reaching us must not strand the UI. */
3328
3389
  async settleIfIdle() {
@@ -3366,11 +3427,16 @@ var Chat = class {
3366
3427
  this.answeringUi.delete(id);
3367
3428
  }
3368
3429
  }
3430
+ aborting = false;
3369
3431
  async abort() {
3370
- await this.pi.send({ type: "abort" }).catch(() => {
3371
- });
3372
- this.live = null;
3373
- this.broadcast(true);
3432
+ if (this.aborting) throw new Error("Stop agent is already in flight.");
3433
+ this.aborting = true;
3434
+ try {
3435
+ await this.pi.send({ type: "abort" }, 15e3);
3436
+ await this.settleIfIdle();
3437
+ } finally {
3438
+ this.aborting = false;
3439
+ }
3374
3440
  }
3375
3441
  /** The newest session entry of a kind (`loops-doctor`, `loops-run`, …), for read-only views. */
3376
3442
  custom(type) {
@@ -4728,7 +4794,7 @@ var withLive = (state) => {
4728
4794
  return state;
4729
4795
  };
4730
4796
  var streams = /* @__PURE__ */ new Set();
4731
- var snapshotOf = (chat, wsId) => withLive({ ...chat.snapshot(), chats: chatsOf(wsId), workspaces: workspaces.list(), ws: wsId, proposals });
4797
+ var snapshotOf = (chat, wsId) => withLive({ ...chat.snapshot(), chats: chatsOf(wsId), workspaces: workspaces.list(), ws: wsId, proposals, configErrors: [...configErrors.values()].filter((e) => e.ws === wsId) });
4732
4798
  manager.onChange((chat) => {
4733
4799
  const space = workspaces.ofChat(chat.id);
4734
4800
  const state = snapshotOf(chat, space.id);
@@ -4736,6 +4802,7 @@ manager.onChange((chat) => {
4736
4802
  });
4737
4803
  var watches = /* @__PURE__ */ new Map();
4738
4804
  var proposals = [];
4805
+ var configErrors = /* @__PURE__ */ new Map();
4739
4806
  var watchOf = (wsId) => {
4740
4807
  const ws = workspaces.get(wsId);
4741
4808
  let w = watches.get(ws.id);
@@ -4753,7 +4820,14 @@ var watchOf = (wsId) => {
4753
4820
  fs9.watch(path9.join(ws.path, ".pi"), (_e, name) => {
4754
4821
  if (name && name !== "loops.json") return;
4755
4822
  clearTimeout(watchTimers.get(ws.id));
4756
- watchTimers.set(ws.id, setTimeout(() => checkConfig(ws.id), 120));
4823
+ watchTimers.set(ws.id, setTimeout(() => {
4824
+ try {
4825
+ checkConfig(ws.id);
4826
+ } catch (e) {
4827
+ configErrors.set(ws.id, { ws: ws.id, file: path9.join(ws.path, ".pi/loops.json"), error: `Could not validate settings: ${e.message}. Fix the file or save settings again.` });
4828
+ notifyProposals();
4829
+ }
4830
+ }, 120));
4757
4831
  });
4758
4832
  } catch {
4759
4833
  }
@@ -4762,12 +4836,18 @@ var watchOf = (wsId) => {
4762
4836
  };
4763
4837
  var watchTimers = /* @__PURE__ */ new Map();
4764
4838
  function notifyProposals() {
4765
- const payload = JSON.stringify({ type: "proposals", proposals });
4766
4839
  for (const s of streams) {
4767
4840
  try {
4768
4841
  s.send("proposals", proposals);
4769
4842
  } catch {
4770
4843
  }
4844
+ const chat = manager.openChats().find((chat2) => chat2.id === s.chat);
4845
+ if (chat) {
4846
+ try {
4847
+ s.send("state", snapshotOf(chat, workspaces.ofChat(chat.id).id), chat.entryCursor);
4848
+ } catch {
4849
+ }
4850
+ }
4771
4851
  }
4772
4852
  }
4773
4853
  function checkConfig(wsId) {
@@ -4780,14 +4860,33 @@ function checkConfig(wsId) {
4780
4860
  return "";
4781
4861
  }
4782
4862
  })();
4863
+ let next;
4864
+ try {
4865
+ next = raw2.trim() ? JSON.parse(raw2) : {};
4866
+ if (!next || typeof next !== "object" || Array.isArray(next)) throw new Error("expected a JSON object");
4867
+ } catch (e) {
4868
+ let recovery = "";
4869
+ try {
4870
+ const backup = `${CONFIG}.invalid-${Date.now()}`;
4871
+ fs9.writeFileSync(backup, raw2);
4872
+ restoreConfig(w.cwd, w.accepted);
4873
+ recovery = ` Last accepted settings restored. Rejected text saved in ${backup}.`;
4874
+ } catch (writeError) {
4875
+ recovery = ` Could not restore settings: ${writeError.message}. Fix ${CONFIG} before running another loop.`;
4876
+ }
4877
+ configErrors.set(wsId, { ws: wsId, file: CONFIG, error: `Invalid config JSON: ${e.message}.${recovery}` });
4878
+ notifyProposals();
4879
+ return;
4880
+ }
4783
4881
  if (isSelfWriting() || isSelfWrite(raw2)) {
4784
- w.accepted = raw2.trim() ? JSON.parse(raw2) : {};
4882
+ w.accepted = next;
4785
4883
  return;
4786
4884
  }
4787
- const next = raw2.trim() ? JSON.parse(raw2) : {};
4885
+ const hadError = configErrors.delete(wsId);
4788
4886
  const changes = diffConfig(w.accepted, next);
4789
4887
  if (!changes.length) {
4790
4888
  w.accepted = next;
4889
+ if (hadError) notifyProposals();
4791
4890
  return;
4792
4891
  }
4793
4892
  restoreConfig(w.cwd, w.accepted);
@@ -4900,7 +4999,7 @@ api.get("/state", async (c) => {
4900
4999
  if (!id) return c.json({ error: "chat required" }, 400);
4901
5000
  const chat = await manager.open(id);
4902
5001
  const ws = workspaces.ofChat(id);
4903
- return c.json(withLive({ ...chat.snapshot(), chats: chatsOf(ws.id), workspaces: workspaces.list(), ws: ws.id, proposals }));
5002
+ return c.json(snapshotOf(chat, ws.id));
4904
5003
  });
4905
5004
  api.post("/chat", async (c) => {
4906
5005
  const body = await c.req.json().catch(() => ({}));
@@ -4976,10 +5075,15 @@ api.post("/rename", async (c) => {
4976
5075
  return c.json({ ok: true, title: chat.title });
4977
5076
  });
4978
5077
  api.post("/abort", async (c) => {
4979
- const { chat: id } = await c.req.json();
4980
- const chat = await manager.open(String(id));
4981
- await chat.abort();
4982
- return c.json({ ok: true });
5078
+ const { chat: id } = await c.req.json().catch(() => ({})) ?? {};
5079
+ if (typeof id !== "string" || !id.trim()) return c.json({ error: "chat required" }, 400);
5080
+ const chat = await manager.open(id);
5081
+ try {
5082
+ await chat.abort();
5083
+ return c.json({ ok: true, requested: true });
5084
+ } catch (e) {
5085
+ return c.json({ error: e.message }, 409);
5086
+ }
4983
5087
  });
4984
5088
  api.post("/chat/retry", async (c) => {
4985
5089
  const { chat: id } = await c.req.json();
@@ -5007,29 +5111,51 @@ api.post("/answer", async (c) => {
5007
5111
  await chat.prompt(`/loop answer ${String(question)} ${String(choice)}`);
5008
5112
  return c.json({ ok: true });
5009
5113
  });
5114
+ var steering = /* @__PURE__ */ new Set();
5010
5115
  api.post("/steer", async (c) => {
5011
- const { chat: id, run: run3, action, rule, attempt, text } = await c.req.json();
5012
- const chat = await manager.open(String(id));
5116
+ const { chat: id, run: run3, action, rule, attempt, text } = await c.req.json().catch(() => ({})) ?? {};
5117
+ const token = (v) => typeof v === "string" && /^[\w.-]+$/.test(v);
5118
+ if (!token(id) || !token(run3)) return c.json({ error: "chat and exact run ID required" }, 400);
5119
+ if (!["pick", "take", "apply", "stop", "clean", "rule", "talk", "more"].includes(action)) return c.json({ error: "valid action required" }, 400);
5120
+ if (["pick", "take", "talk"].includes(action) && !token(attempt)) return c.json({ error: "attempt required (durable name, e.g. R1-safe)" }, 400);
5121
+ if (action === "rule" && (typeof rule !== "string" || !rule.trim())) return c.json({ error: "rule required" }, 400);
5122
+ if (action === "talk" && (typeof text !== "string" || !text.trim())) return c.json({ error: "text required" }, 400);
5123
+ if (action === "more" && text !== void 0 && (!Number.isInteger(text) || text < 1 || text > 100)) return c.json({ error: "more rounds must be an integer from 1 to 100" }, 400);
5124
+ const chat = await manager.open(id);
5125
+ if (!chat.hasRun(run3)) return c.json({ error: "run does not belong to this chat" }, 404);
5126
+ if (action === "stop" && !chat.snapshot().runs.find((r) => r.id === run3)?.alive) return c.json({ error: "Selected run is not active. Read its latest session state." }, 409);
5127
+ if (["pick", "take", "talk"].includes(action) && !chat.snapshot().runs.find((r) => r.id === run3)?.rounds.some((r) => r.attempts.some((a) => a.name === attempt))) return c.json({ error: "unknown attempt for selected run" }, 400);
5128
+ const key = `${id}:${run3}:${action}`;
5129
+ if (steering.has(key)) return c.json({ error: "This action is already in flight." }, 409);
5130
+ steering.add(key);
5013
5131
  const slash = (cmd) => chat.prompt(cmd);
5014
- switch (String(action)) {
5015
- case "pick":
5016
- case "take":
5017
- return c.json({ ok: true, sent: await slash(`/loop take ${attempt} ${run3}`) });
5018
- case "apply":
5019
- return c.json({ ok: true, sent: await slash(`/loop apply`) });
5020
- case "stop":
5021
- return c.json({ ok: true, sent: await slash(`/loop stop`) });
5022
- case "clean":
5023
- return c.json({ ok: true, sent: await slash(`/loop clean ${run3}`) });
5024
- case "rule":
5025
- return c.json({ ok: true, sent: await slash(`/loop rule ${run3} ${rule}`) });
5026
- case "talk":
5027
- return c.json({ ok: true, sent: await slash(`/loop talk ${attempt} ${text}`) });
5028
- // everything else is a decision only the agent can make (another round, a different direction)
5029
- case "more":
5030
- return c.json({ ok: true, sent: await slash(`Continue loop ${run3} with ${Number(text) || 2} more rounds. Check loop_status first, then call loop({ from: "${run3}", rounds: ${Number(text) || 2} }).`) });
5031
- default:
5032
- return c.json({ error: `unknown action ${action}` }, 400);
5132
+ try {
5133
+ switch (String(action)) {
5134
+ case "pick":
5135
+ case "take":
5136
+ return c.json({ ok: true, sent: await slash(`/loop take ${attempt} ${run3}`) });
5137
+ case "apply":
5138
+ return c.json({ ok: true, sent: await slash(`/loop apply ${run3}`) });
5139
+ // Pi executes extension commands before its streaming queue. Do not send this as a queued follow-up:
5140
+ // a background round must remain independently stoppable while the parent is in a long-running tool.
5141
+ case "stop":
5142
+ return c.json({ ok: true, requested: true, sent: await slash(`/loop stop ${run3}`) });
5143
+ case "clean":
5144
+ return c.json({ ok: true, sent: await slash(`/loop clean ${run3}`) });
5145
+ case "rule":
5146
+ return c.json({ ok: true, sent: await slash(`/loop rule ${run3} ${rule}`) });
5147
+ case "talk":
5148
+ return c.json({ ok: true, sent: await slash(`/loop talk ${attempt} ${text}`) });
5149
+ // everything else is a decision only the agent can make (another round, a different direction)
5150
+ case "more":
5151
+ return c.json({ ok: true, sent: await slash(`Continue loop ${run3} with ${Number(text) || 2} more rounds. Check loop_status first, then call loop({ from: "${run3}", rounds: ${Number(text) || 2} }).`) });
5152
+ default:
5153
+ return c.json({ error: `unknown action ${action}` }, 400);
5154
+ }
5155
+ } catch (e) {
5156
+ return c.json({ error: e.message }, 409);
5157
+ } finally {
5158
+ steering.delete(key);
5033
5159
  }
5034
5160
  });
5035
5161
  api.get("/ui", async (c) => {
@@ -5132,18 +5258,20 @@ api.post("/proposals/:id/keep", (c) => {
5132
5258
  api.get("/config", (c) => {
5133
5259
  const ws = wsOf(c.req.query("chat"));
5134
5260
  watchOf(ws.id);
5135
- return c.json({ cwd: ws.path, ws: ws.id, keys: effectiveConfig(ws.path) });
5261
+ return c.json({ cwd: ws.path, ws: ws.id, keys: effectiveConfig(ws.path), error: configErrors.get(ws.id)?.error });
5136
5262
  });
5137
5263
  api.post("/config", async (c) => {
5138
5264
  const { key, value, chat } = await c.req.json();
5139
5265
  const ws = wsOf(chat);
5140
5266
  try {
5141
5267
  writeSetting(ws.path, String(key), value);
5268
+ configErrors.delete(ws.id);
5142
5269
  const w = watchOf(ws.id);
5143
5270
  try {
5144
5271
  w.accepted = JSON.parse(fs9.readFileSync(path9.join(ws.path, ".pi", "loops.json"), "utf8"));
5145
5272
  } catch {
5146
5273
  }
5274
+ notifyProposals();
5147
5275
  return c.json({ ok: true });
5148
5276
  } catch (e) {
5149
5277
  return c.json({ ok: false, error: String(e.message ?? e) }, 400);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@computesdk/loops",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Loops for people who do not live in a terminal: one command starts a localhost UI that runs, watches and steers hill-climbing agents in your repos.",
6
6
  "keywords": [
@@ -37,8 +37,8 @@
37
37
  "start": "node --experimental-strip-types server/index.ts",
38
38
  "check": "tsc --noEmit && tsc --noEmit -p tsconfig.client.json",
39
39
  "shots": "node shots.mjs",
40
- "test": "esbuild test/compare.ts --bundle --platform=node --format=cjs --outfile=node_modules/.tmp/compare.cjs --loader:.tsx=tsx --jsx=automatic --log-level=error && node node_modules/.tmp/compare.cjs && node --experimental-strip-types test/runs.ts && node --experimental-strip-types test/workspaces.ts && node --experimental-strip-types test/chat-lifecycle.ts && node --experimental-strip-types test/workspace-api.ts && node --experimental-strip-types test/api.ts && node --experimental-strip-types test/cli.ts && node --experimental-strip-types test/pi-startup.ts",
41
- "test:browser": "node test/workspace-browser.mjs && node test/pi-dialog-browser.mjs"
40
+ "test": "esbuild test/compare.ts --bundle --platform=node --format=cjs --outfile=node_modules/.tmp/compare.cjs --loader:.tsx=tsx --jsx=automatic --log-level=error && node node_modules/.tmp/compare.cjs && node --experimental-strip-types test/runs.ts && node --experimental-strip-types test/workspaces.ts && node --experimental-strip-types test/chat-lifecycle.ts && node --experimental-strip-types test/workspace-api.ts && node --experimental-strip-types test/api.ts && node --experimental-strip-types test/stop-api.ts && node --experimental-strip-types test/cli.ts && node --experimental-strip-types test/pi-startup.ts",
41
+ "test:browser": "node test/workspace-browser.mjs && node test/pi-dialog-browser.mjs && node test/stop-browser.mjs"
42
42
  },
43
43
  "dependencies": {
44
44
  "@computesdk/pi-loops": "^1.0.7",
@@ -2,6 +2,14 @@
2
2
  // Pi's session entries (`loops-run`, `loops-round`, `loops-question`, tool results), the run's state file and
3
3
  // the worktrees it owns. Nothing polls: a live round arrives as `tool_execution_update` from the RPC child.
4
4
 
5
+ /** Engine actions. Attempt is the durable name (R1-safe), never the display letter (A). */
6
+ export type SteerAction =
7
+ | { action: "pick" | "take"; attempt: string }
8
+ | { action: "apply" | "stop" | "clean" }
9
+ | { action: "rule"; rule: string }
10
+ | { action: "talk"; attempt: string; text: string }
11
+ | { action: "more"; text?: number };
12
+
5
13
  export type RunAttempt = {
6
14
  /** `run:rN-L` */
7
15
  id: string;
@@ -12,6 +20,7 @@ export type RunAttempt = {
12
20
  style: string;
13
21
  /** running | done | error | unchanged | aborted | timeout | budget */
14
22
  status: string;
23
+ cut?: string;
15
24
  score?: number;
16
25
  kept?: boolean;
17
26
  humanPick?: boolean;
@@ -58,6 +67,7 @@ export type RunRound = {
58
67
  shots?: Record<string, string[]>;
59
68
  /** the judge never saw the images: nothing in this round was compared */
60
69
  inconclusive?: boolean;
70
+ status?: string;
61
71
  /** an attempt committed into your checkout; `restored` says whether it was put back */
62
72
  escaped?: { from: string; to: string; restored?: boolean } | null;
63
73
  /** candidates whose stamped code the judge could not read back */
@@ -81,6 +91,8 @@ export type Question = {
81
91
  label?: string;
82
92
  options: { label: string; steer?: Record<string, unknown> }[];
83
93
  answered?: { i?: number; auto?: string; t: number };
94
+ /** The decision deadline passed without an answer. */
95
+ expired?: boolean;
84
96
  };
85
97
 
86
98
  export type Run = {
@@ -97,7 +109,10 @@ export type Run = {
97
109
  stopReason?: string;
98
110
  rules: { text: string; source: string }[];
99
111
  rounds: RunRound[];
100
- best: { round: number; label: string; score?: number; shot?: string; live?: string; byHuman?: boolean };
112
+ best: { round: number; label: string; score?: number; shot?: string; live?: string; byHuman?: boolean;
113
+ /** The engine's successful apply commit; absent means unknown, not proof of an unapplied winner. */
114
+ applied?: string;
115
+ };
101
116
  scores: number[];
102
117
  maxRounds: number;
103
118
  tokens: number;
@@ -132,7 +147,7 @@ export type ChatSummary = {
132
147
  /** the newest open question, if any */
133
148
  question?: { kind: string; round?: number; title: string };
134
149
  lastT: number;
135
- latest?: { name: string; status: string; kind: "visual" | "bench"; score?: number; unit?: string; rounds: number };
150
+ latest?: { name: string; status: string; kind: "visual" | "bench"; score?: number; unit?: string; rounds: number; outcome?: string };
136
151
  /** a run whose process went away: it says running, nothing is working */
137
152
  };
138
153
 
@@ -171,10 +186,12 @@ export type ChatState = {
171
186
  items: ParentItem[];
172
187
  live: ParentLive | null;
173
188
  error?: string;
189
+ notices?: { t: number; level: string; message: string }[];
174
190
  runs: Run[];
175
191
  model?: string | null;
176
192
  /** open questions for chats that are not loaded, so the sidebar can count them */
177
193
  busyElsewhere?: boolean;
178
194
  /** config writes waiting for a yes/no; the file is unchanged until one */
179
195
  proposals?: ConfigProposal[];
196
+ configErrors?: { ws: string; file: string; error: string }[];
180
197
  };
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./index-Bejffjbf.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-TICWLB2K-iCYUhmUO.js";import{_ as i}from"./index-Bejffjbf.js";import"./chunk-5VM5RSS4-BThX-upu.js";import"./chunk-XXDRQBXY-j5Ron3J8.js";import"./chunk-POPQ4Y6H-eA1aBSf8.js";import"./chunk-F27PBJKO-BkZOip-K.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-TICWLB2K-iCYUhmUO.js";import{_ as i}from"./index-Bejffjbf.js";import"./chunk-5VM5RSS4-BThX-upu.js";import"./chunk-XXDRQBXY-j5Ron3J8.js";import"./chunk-POPQ4Y6H-eA1aBSf8.js";import"./chunk-F27PBJKO-BkZOip-K.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};