@tea-agent/loop-agent 0.40.0-next.12 → 0.40.0-next.14

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 (116) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +2 -2
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/cli/command-definitions.js +9 -5
  5. package/dist/cli/program.js +38 -3
  6. package/dist/commands/init-model-catalog.js +22 -0
  7. package/dist/commands/task-advance.js +23 -1
  8. package/dist/commands/task-source-prepare.js +5 -0
  9. package/dist/commands/task-status.js +1 -0
  10. package/dist/shared/operator/capabilities.js +9 -1
  11. package/dist/worker/console/chat/assistant-content.js +7 -0
  12. package/dist/worker/console/chat/chat-event-store.js +82 -11
  13. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  14. package/dist/worker/console/chat/goals.js +300 -0
  15. package/dist/worker/console/chat/pi-runtime.js +140 -4
  16. package/dist/worker/console/chat/repo-browser.js +94 -12
  17. package/dist/worker/console/chat/routes.js +482 -29
  18. package/dist/worker/console/chat/session-catalog.js +48 -0
  19. package/dist/worker/console/chat/session-store.js +27 -18
  20. package/dist/worker/console/chat/shortcuts.js +7 -0
  21. package/dist/worker/console/chat/turn-process.js +32 -3
  22. package/dist/worker/console/operator-actions.js +16 -2
  23. package/dist/worker/console/operator-selection.js +6 -2
  24. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  25. package/dist/worker/console/pi-plugins.js +100 -0
  26. package/dist/worker/console/routes.js +221 -6
  27. package/dist/worker/console/server.js +172 -42
  28. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C_hkwt2J.js → abnfDiagram-N423BO3Z-B0gRLXJs.js} +1 -1
  29. package/dist/worker/console/static/assets/{arc-s2Q6r96B.js → arc-DldY0Hsk.js} +1 -1
  30. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CtgsrwU1.js → architectureDiagram-T3A2C74G-4xHhMRM9.js} +1 -1
  31. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DZ-A7EiJ.js → blockDiagram-VBNYF7ZC-DPym7n0M.js} +1 -1
  32. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D2qv0Nyo.js → c4Diagram-5PPSVZJV-cSFzY5yS.js} +1 -1
  33. package/dist/worker/console/static/assets/channel-4xBA5kwn.js +1 -0
  34. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D-UYpSaW.js → chunk-2GRJ4B5K-w-64RMuc.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DAS3jrrt.js → chunk-2Q5K7J3B-Bvue--jh.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CSbuRcGB.js → chunk-5RXB4S5H-DxqkQnCh.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BDH1ZO40.js → chunk-5VM5RSS4-C83gOKLK.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dc2cqQIb.js → chunk-6Q2QTUOP-C_V6tKAV.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CkZuemIW.js → chunk-GF5L2VYU-CPvZQuL9.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-URwjQnte.js → chunk-JWPE2WC7-w31Z_ATM.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-04RHylJK.js → chunk-KBJHAD2P-DermQC9f.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CvQX56Hh.js → chunk-RYQCIY6F-CmwaYcnB.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-dUF7qura.js → chunk-XXDRQBXY-DLwZqUBf.js} +1 -1
  44. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CkefOueI.js +1 -0
  45. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CkefOueI.js +1 -0
  46. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-COwbfxJE.js → cose-bilkent-JH36ORCC-Dzh7bT73.js} +1 -1
  47. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CaBEJekQ.js → cynefin-VYW2F7L2-CfOcBHtp.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-COWUBdet.js → cynefinDiagram-MW4NZA55-D0EbCVDZ.js} +1 -1
  49. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BLxVqTn4.js → dagre-VZM6K2ZE-MfOFA6PZ.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-WL0yKfEQ.js → diagram-7IWD3JNH-BOa5-9PN.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DaVkKCle.js → diagram-B4RE2ZJO-B6zvJ-aL.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DdTgnRGf.js → diagram-LBJQPF4R-BKwr5bRF.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-PG2Axo6A.js → diagram-Q27KOJAE-PLI5QLpX.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-UB23O5K3-6vuQzlzI.js → diagram-UB23O5K3-BgYYME3o.js} +1 -1
  55. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BpXeBz-g.js → ebnfDiagram-BXEA7PRR-D-SpjXk2.js} +1 -1
  56. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-PxtUnaa1.js → erDiagram-JOGREHBK--JHpEOka.js} +1 -1
  57. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B--Qgvs1.js → flowDiagram-UKHOOZJN-CbkQgWUz.js} +1 -1
  58. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BIfQCgtx.js → ganttDiagram-PKOTCBZU-CTZMAMVE.js} +1 -1
  59. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BRaX_E4j.js → gitGraphDiagram-DS77QQ5N-BaUGSDig.js} +1 -1
  60. package/dist/worker/console/static/assets/index-C4mO0i6P.js +437 -0
  61. package/dist/worker/console/static/assets/index-C86Y55Or.css +1 -0
  62. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Cd93r4Ab.js → infoDiagram-6WML65LV-DA3WbWjf.js} +1 -1
  63. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C4c80iHt.js → ishikawaDiagram-WSZJBQD7-DbhI82U4.js} +1 -1
  64. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Ch5ajf3s.js → journeyDiagram-NVQOT4AX-CJKA3OBG.js} +1 -1
  65. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DbpyW1Ut.js → kanban-definition-27J2QSJJ-CF6dIVRa.js} +1 -1
  66. package/dist/worker/console/static/assets/{linear-BHVjBnTW.js → linear-D4dEtOUL.js} +1 -1
  67. package/dist/worker/console/static/assets/{mermaid.core-DO70kJ34.js → mermaid.core-ClhMic6O.js} +5 -5
  68. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-_P44Zpvs.js → mindmap-definition-FAOFIHXS-NfRin7S2.js} +1 -1
  69. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bgi9FmEd.js → pegDiagram-VL7TDLO6-DlBtBwW-.js} +1 -1
  70. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C9InDfJn.js → pieDiagram-7S7Q4E2Y-DdIkNHZO.js} +1 -1
  71. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DtKrUouv.js → quadrantDiagram-CIZ2JOQS-YD8T98WX.js} +1 -1
  72. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-zgAD_6Yo.js → railroadDiagram-AXF67PYL-CS4e5zGK.js} +1 -1
  73. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQNO6kj4.js → requirementDiagram-LRYGKXZP-BpbWoL9r.js} +1 -1
  74. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-knKu111P.js → sankeyDiagram-W5VNT64P-DvWy-Mc9.js} +1 -1
  75. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-D6tshiQ6.js → sequenceDiagram-SI44F4Z6-DmoXH5pJ.js} +1 -1
  76. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BMIWJcir.js → sizeCapture-X5ZJPWSS-BY89V1SW.js} +1 -1
  77. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BP9wRDqc.js → stateDiagram-OKZ733FA-DaK2Iuqr.js} +1 -1
  78. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DLc5WD-z.js +1 -0
  79. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DEkM5Dhe.js → swimlanes-SLNWSIFB-VVcozat-.js} +2 -2
  80. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DhjxpI7d.js +8 -0
  81. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ByBKsL35.js → timeline-definition-Z64GVDOM-D4l1E4Mu.js} +1 -1
  82. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-waFbNCUy.js → vennDiagram-T6HMQDX7-CJmewqcq.js} +1 -1
  83. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-1ljiY0eZ.js → wardleyDiagram-T6FBY63Y-CsCCYorj.js} +1 -1
  84. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B4ZLHr_L.js → xychartDiagram-ELKLHX3M-CwjILi0N.js} +1 -1
  85. package/dist/worker/console/static/index.html +2 -2
  86. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  87. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  88. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  89. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +24 -19
  90. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  91. package/dist/worker/console/static-src/operator-chat/useChatThread.js +8 -1
  92. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  93. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  94. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  95. package/dist/worker/console/workspace-context.js +28 -3
  96. package/dist/worker/console/workspace-registry.js +333 -60
  97. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  98. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  99. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  100. package/dist/worker/observe/static/operator-chrome.css +23 -4
  101. package/dist/worker/observe/static/operator-chrome.js +6 -2
  102. package/dist/worker/observe/static/styles.css +78 -25
  103. package/dist/worker/observe/static/views/dag-inspector.js +2 -1
  104. package/dist/worker/observe/static/views/dags.js +7 -5
  105. package/dist/workflows/dag/init-hybrid.js +3 -3
  106. package/docs/templates/backend-test-dag.json +3 -3
  107. package/docs/templates/init-managed-agents.md +1 -1
  108. package/package.json +1 -1
  109. package/skills/loop-agent/references/command-reference.md +15 -1
  110. package/dist/worker/console/static/assets/channel-BluaMT3-.js +0 -1
  111. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-H-ZpSafr.js +0 -1
  112. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-H-ZpSafr.js +0 -1
  113. package/dist/worker/console/static/assets/index-Crrgl8Mx.css +0 -1
  114. package/dist/worker/console/static/assets/index-vPsp-idE.js +0 -437
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-ClEhytQG.js +0 -1
  116. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPg5IMuu.js +0 -8
@@ -0,0 +1,100 @@
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import catalogData from "./pi-plugins.catalog.json" with { type: "json" };
6
+ export function getPiAgentDir() {
7
+ const envDir = process.env.PI_CODING_AGENT_DIR;
8
+ return envDir ? envDir : path.join(os.homedir(), ".pi", "agent");
9
+ }
10
+ function npmRoot() {
11
+ return path.join(getPiAgentDir(), "npm");
12
+ }
13
+ function nodeModulesRoot() {
14
+ return path.join(npmRoot(), "node_modules");
15
+ }
16
+ function settingsPath() {
17
+ return path.join(getPiAgentDir(), "settings.json");
18
+ }
19
+ function readSettings() {
20
+ try {
21
+ if (!existsSync(settingsPath()))
22
+ return {};
23
+ return JSON.parse(readFileSync(settingsPath(), "utf8"));
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ function packageNameFromSpec(spec) {
30
+ return spec.startsWith("npm:") ? spec.slice(4) : spec;
31
+ }
32
+ function listInstalledPackageNames() {
33
+ const settings = readSettings();
34
+ return (settings.packages ?? []).map(packageNameFromSpec);
35
+ }
36
+ function isPackageInstalledOnDisk(pkg) {
37
+ return existsSync(path.join(nodeModulesRoot(), ...pkg.split("/")));
38
+ }
39
+ /** 目录 + 每个插件的已装状态(settings.packages 声明 + node_modules 存在)。 */
40
+ export function listPiPlugins() {
41
+ const installed = new Set(listInstalledPackageNames());
42
+ const plugins = catalogData.plugins.map((p) => ({
43
+ ...p,
44
+ installed: installed.has(p.name) && isPackageInstalledOnDisk(p.name),
45
+ }));
46
+ return { summary: catalogData.summary, plugins };
47
+ }
48
+ function runPi(args) {
49
+ return new Promise((resolve) => {
50
+ mkdirSync(getPiAgentDir(), { recursive: true });
51
+ // Windows 上 pi 是 pi.cmd(无 .exe),.cmd/.bat 只能 shell:true 解析。
52
+ const child = spawn("pi", args, {
53
+ stdio: ["ignore", "pipe", "pipe"],
54
+ cwd: getPiAgentDir(),
55
+ shell: process.platform === "win32",
56
+ });
57
+ let stdout = "";
58
+ let stderr = "";
59
+ child.stdout.on("data", (d) => (stdout += d.toString("utf8")));
60
+ child.stderr.on("data", (d) => (stderr += d.toString("utf8")));
61
+ child.on("close", (code) => resolve({ code: code ?? -1, stdout, stderr }));
62
+ child.on("error", (err) => resolve({ code: -1, stdout, stderr: String(err?.message ?? err) }));
63
+ });
64
+ }
65
+ /** 校验安装目标是否在目录中,且该包确实声明了 pi.extensions(可安装为 Pi 扩展)。 */
66
+ export function findCatalogPlugin(name) {
67
+ return catalogData.plugins.find((p) => p.name === name);
68
+ }
69
+ /** 全局(用户级 ~/.pi/agent)安装:调用 `pi install npm:<name>`。 */
70
+ export async function installPiPlugin(name) {
71
+ const entry = findCatalogPlugin(name);
72
+ if (!entry) {
73
+ return { ok: false, error: `包 ${name} 不在插件目录中` };
74
+ }
75
+ if (!entry.valid) {
76
+ return {
77
+ ok: false,
78
+ error: `包 ${name} 未声明 pi.extensions,无法作为 Pi 扩展使用`,
79
+ };
80
+ }
81
+ const result = await runPi(["install", `npm:${name}`]);
82
+ if (result.code !== 0) {
83
+ return {
84
+ ok: false,
85
+ error: `pi install 失败 (exit ${result.code}): ${(result.stderr || result.stdout).slice(0, 400)}`,
86
+ };
87
+ }
88
+ return { ok: true };
89
+ }
90
+ /** 全局卸载:调用 `pi remove npm:<name>`。 */
91
+ export async function uninstallPiPlugin(name) {
92
+ const result = await runPi(["remove", `npm:${name}`]);
93
+ if (result.code !== 0) {
94
+ return {
95
+ ok: false,
96
+ error: `pi remove 失败 (exit ${result.code}): ${(result.stderr || result.stdout).slice(0, 400)}`,
97
+ };
98
+ }
99
+ return { ok: true };
100
+ }
@@ -8,6 +8,7 @@ import { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
8
8
  import { ROUTES } from "../observe/routes.js";
9
9
  import { evaluateMutationGate, injectConfirmationTokenScript, isMutationMethod, } from "./security.js";
10
10
  import { listDagRunsForRecovery } from "./resolve-dag-run-for-task.js";
11
+ import { installPiPlugin, listPiPlugins, uninstallPiPlugin, } from "./pi-plugins.js";
11
12
  import { pickNativeWorkspaceDirectory } from "./native-directory-picker.js";
12
13
  export function sendJson(res, status, body, extraHeaders) {
13
14
  const payload = JSON.stringify(body);
@@ -462,6 +463,94 @@ async function handleNightAuxTickerPost(req, res, ctx) {
462
463
  const status = ticker.setEnabled(body.enabled);
463
464
  sendJson(res, 200, { ok: true, ticker: status });
464
465
  }
466
+ async function handlePiPluginInstall(req, res) {
467
+ let body;
468
+ try {
469
+ body = (await readJsonBody(req));
470
+ }
471
+ catch (error) {
472
+ sendJson(res, 400, {
473
+ ok: false,
474
+ error: {
475
+ code: "INVALID_INPUT",
476
+ message: error instanceof Error ? error.message : String(error),
477
+ },
478
+ });
479
+ return;
480
+ }
481
+ const name = typeof body.name === "string" ? body.name.trim() : "";
482
+ if (!name) {
483
+ sendJson(res, 400, {
484
+ ok: false,
485
+ error: { code: "INVALID_INPUT", message: "missing package name" },
486
+ });
487
+ return;
488
+ }
489
+ try {
490
+ const result = await installPiPlugin(name);
491
+ if (!result.ok) {
492
+ sendJson(res, 400, {
493
+ ok: false,
494
+ error: { code: "INSTALL_FAILED", message: result.error },
495
+ });
496
+ return;
497
+ }
498
+ sendJson(res, 200, { ok: true });
499
+ }
500
+ catch (error) {
501
+ sendJson(res, 500, {
502
+ ok: false,
503
+ error: {
504
+ code: "INTERNAL_ERROR",
505
+ message: error instanceof Error ? error.message : String(error),
506
+ },
507
+ });
508
+ }
509
+ }
510
+ async function handlePiPluginUninstall(req, res) {
511
+ let body;
512
+ try {
513
+ body = (await readJsonBody(req));
514
+ }
515
+ catch (error) {
516
+ sendJson(res, 400, {
517
+ ok: false,
518
+ error: {
519
+ code: "INVALID_INPUT",
520
+ message: error instanceof Error ? error.message : String(error),
521
+ },
522
+ });
523
+ return;
524
+ }
525
+ const name = typeof body.name === "string" ? body.name.trim() : "";
526
+ if (!name) {
527
+ sendJson(res, 400, {
528
+ ok: false,
529
+ error: { code: "INVALID_INPUT", message: "missing package name" },
530
+ });
531
+ return;
532
+ }
533
+ try {
534
+ const result = await uninstallPiPlugin(name);
535
+ if (!result.ok) {
536
+ sendJson(res, 400, {
537
+ ok: false,
538
+ error: { code: "UNINSTALL_FAILED", message: result.error },
539
+ });
540
+ return;
541
+ }
542
+ sendJson(res, 200, { ok: true });
543
+ }
544
+ catch (error) {
545
+ sendJson(res, 500, {
546
+ ok: false,
547
+ error: {
548
+ code: "INTERNAL_ERROR",
549
+ message: error instanceof Error ? error.message : String(error),
550
+ },
551
+ });
552
+ }
553
+ }
465
554
  export async function handleConsoleOperatorRequest(req, res, ctx) {
466
555
  const method = (req.method ?? "GET").toUpperCase();
467
556
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -495,6 +584,10 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
495
584
  await handleNightAuxTickerGet(req, res, ctx);
496
585
  return true;
497
586
  }
587
+ if (method === "GET" && pathname === "/api/operator/v1/pi-plugins") {
588
+ sendJson(res, 200, { ok: true, data: listPiPlugins() });
589
+ return true;
590
+ }
498
591
  const opMatch = pathname.match(/^\/api\/operator\/v1\/operations\/([^/]+)(?:\/(events))?$/);
499
592
  if (opMatch) {
500
593
  const operationId = decodeURIComponent(opMatch[1]);
@@ -526,6 +619,14 @@ export async function handleConsoleOperatorRequest(req, res, ctx) {
526
619
  await handleNightAuxTickerPost(req, res, ctx);
527
620
  return true;
528
621
  }
622
+ if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/install") {
623
+ await handlePiPluginInstall(req, res);
624
+ return true;
625
+ }
626
+ if (method === "POST" && pathname === "/api/operator/v1/pi-plugins/uninstall") {
627
+ await handlePiPluginUninstall(req, res);
628
+ return true;
629
+ }
529
630
  // Allow other operator POSTs only under known routes; unknown → 404
530
631
  sendJson(res, 404, {
531
632
  ok: false,
@@ -587,9 +688,11 @@ function pendingReadinessSummary(inspectReady) {
587
688
  * POST /api/operator/v1/workspaces/pick — host native folder chooser
588
689
  * POST /api/operator/v1/workspaces — body { "name": "<label>",
589
690
  * "directoryPath": "<dir>" }
691
+ * PATCH /api/operator/v1/workspaces/:workspaceId — body { "beforeWorkspaceId":
692
+ * "<id>" | null }
590
693
  * DELETE /api/operator/v1/workspaces/:workspaceId — soft remove (restore on re-add)
591
694
  *
592
- * POST/DELETE are mutations and pass the Console mutation gate. GET carries no
695
+ * POST/PATCH/DELETE are mutations and pass the Console mutation gate. GET carries no
593
696
  * workspace selection parameter.
594
697
  *
595
698
  * AC-006 (2026-08-23): GET is NON-BLOCKING — registry + process-level
@@ -598,8 +701,53 @@ function pendingReadinessSummary(inspectReady) {
598
701
  * requests. A hanging probe can never delay the 200 list.
599
702
  */
600
703
  export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathname, method, readinessDeps) {
704
+ if (method === "GET" &&
705
+ pathname === "/api/operator/v1/workspaces/events") {
706
+ res.writeHead(200, {
707
+ "Content-Type": "text/event-stream; charset=utf-8",
708
+ "Cache-Control": "no-cache, no-transform",
709
+ Connection: "keep-alive",
710
+ "X-Accel-Buffering": "no",
711
+ });
712
+ const sendRevision = (event, revision) => {
713
+ if (res.destroyed || res.writableEnded)
714
+ return;
715
+ res.write(`id: ${revision}\nevent: ${event}\ndata: ${JSON.stringify({ schemaVersion: 1, revision })}\n\n`);
716
+ };
717
+ let baselineSent = false;
718
+ let pendingRevision = 0;
719
+ const unsubscribe = registry.subscribe((revision) => {
720
+ if (!baselineSent)
721
+ pendingRevision = Math.max(pendingRevision, revision);
722
+ else
723
+ sendRevision("workspace_registry_changed", revision);
724
+ });
725
+ const initial = await registry.snapshotVisible();
726
+ sendRevision("workspace_registry_ready", initial.revision);
727
+ baselineSent = true;
728
+ if (pendingRevision > initial.revision) {
729
+ sendRevision("workspace_registry_changed", pendingRevision);
730
+ }
731
+ const heartbeat = setInterval(() => {
732
+ if (!res.destroyed && !res.writableEnded)
733
+ res.write(": keepalive\n\n");
734
+ }, 15_000);
735
+ heartbeat.unref();
736
+ let closed = false;
737
+ const close = () => {
738
+ if (closed)
739
+ return;
740
+ closed = true;
741
+ clearInterval(heartbeat);
742
+ unsubscribe();
743
+ };
744
+ req.once("aborted", close);
745
+ res.once("close", close);
746
+ return;
747
+ }
601
748
  if (method === "GET" && pathname === "/api/operator/v1/workspaces") {
602
- const records = await registry.listVisible();
749
+ const snapshot = await registry.snapshotVisible();
750
+ const records = snapshot.workspaces;
603
751
  const inspectReady = readinessDeps.inspectReady;
604
752
  const readinessCache = readinessDeps.readinessCache ?? defaultWorkspaceReadinessCache;
605
753
  const workspaces = records.map((record) => {
@@ -611,6 +759,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
611
759
  repoFingerprint: record.repoFingerprint,
612
760
  createdAt: record.createdAt,
613
761
  lastAccessedAt: record.lastAccessedAt,
762
+ sessionIds: record.sessionIds,
614
763
  readiness: cached
615
764
  ? {
616
765
  state: cached.state,
@@ -622,7 +771,8 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
622
771
  // Respond FIRST, then refresh readiness in the background (AC-006).
623
772
  sendJson(res, 200, {
624
773
  ok: true,
625
- schemaVersion: 1,
774
+ schemaVersion: 2,
775
+ revision: snapshot.revision,
626
776
  workspaces,
627
777
  });
628
778
  void Promise.all(records.map(async (record) => {
@@ -789,8 +939,73 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
789
939
  });
790
940
  return;
791
941
  }
792
- const removeMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
793
- if (method === "DELETE" && removeMatch) {
942
+ const workspaceMatch = pathname.match(/^\/api\/operator\/v1\/workspaces\/([^/]+)$/);
943
+ if (method === "PATCH" && workspaceMatch) {
944
+ const failure = evaluateMutationGate(req, {
945
+ cookieName: deps.bootToken.cookieName,
946
+ bootToken: deps.bootToken.value,
947
+ confirmationToken: deps.bootToken.confirmationToken,
948
+ consoleOrigin: deps.getConsoleOrigin(),
949
+ allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
950
+ });
951
+ if (failure) {
952
+ sendJson(res, failure.status, {
953
+ ok: false,
954
+ error: { code: failure.code, message: failure.message },
955
+ });
956
+ return;
957
+ }
958
+ let body;
959
+ try {
960
+ body = (await readJsonBody(req));
961
+ }
962
+ catch (error) {
963
+ sendJson(res, 400, {
964
+ ok: false,
965
+ error: {
966
+ code: "INVALID_INPUT",
967
+ message: error instanceof Error ? error.message : String(error),
968
+ },
969
+ });
970
+ return;
971
+ }
972
+ if (body.beforeWorkspaceId !== null &&
973
+ typeof body.beforeWorkspaceId !== "string") {
974
+ sendJson(res, 400, {
975
+ ok: false,
976
+ error: {
977
+ code: "INVALID_INPUT",
978
+ message: "beforeWorkspaceId must be a workspace id or null",
979
+ },
980
+ });
981
+ return;
982
+ }
983
+ const workspaceId = decodeURIComponent(workspaceMatch[1]);
984
+ const beforeWorkspaceId = typeof body.beforeWorkspaceId === "string"
985
+ ? body.beforeWorkspaceId
986
+ : null;
987
+ const reordered = await registry.insertBefore(workspaceId, beforeWorkspaceId);
988
+ if (!reordered.ok) {
989
+ sendJson(res, 404, {
990
+ ok: false,
991
+ error: {
992
+ code: reordered.code,
993
+ message: reordered.code === "ANCHOR_NOT_FOUND"
994
+ ? `unknown workspace anchor: ${beforeWorkspaceId}`
995
+ : `unknown workspace: ${workspaceId}`,
996
+ },
997
+ });
998
+ return;
999
+ }
1000
+ sendJson(res, 200, {
1001
+ ok: true,
1002
+ workspaceId,
1003
+ beforeWorkspaceId,
1004
+ changed: reordered.changed,
1005
+ });
1006
+ return;
1007
+ }
1008
+ if (method === "DELETE" && workspaceMatch) {
794
1009
  const failure = evaluateMutationGate(req, {
795
1010
  cookieName: deps.bootToken.cookieName,
796
1011
  bootToken: deps.bootToken.value,
@@ -805,7 +1020,7 @@ export async function handleWorkspaceRoutes(req, res, registry, deps, url, pathn
805
1020
  });
806
1021
  return;
807
1022
  }
808
- const workspaceId = decodeURIComponent(removeMatch[1]);
1023
+ const workspaceId = decodeURIComponent(workspaceMatch[1]);
809
1024
  const removed = await registry.remove(workspaceId);
810
1025
  if (!removed) {
811
1026
  sendJson(res, 404, {