@tea-agent/loop-agent 0.44.0-next.4 → 0.44.0-next.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 (100) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/pi-sdk-executor.js +7 -7
  4. package/dist/executors/shell-write-guard.js +26 -3
  5. package/dist/infrastructure/console/operation-store.js +46 -14
  6. package/dist/shared/file-identity.js +23 -0
  7. package/dist/shared/git-workspace-status.js +480 -0
  8. package/dist/shared/path-safety.js +2 -2
  9. package/dist/shared/wizard-local-telemetry.js +2 -1
  10. package/dist/worker/console/chat/context-insights.js +723 -0
  11. package/dist/worker/console/chat/pi-runtime.js +217 -18
  12. package/dist/worker/console/chat/repo-browser.js +52 -4
  13. package/dist/worker/console/chat/routes.js +150 -58
  14. package/dist/worker/console/chat/shortcuts.js +7 -0
  15. package/dist/worker/console/console-update-runtime.js +2 -2
  16. package/dist/worker/console/operator-user-error.js +28 -1
  17. package/dist/worker/console/pi-readiness.js +4 -4
  18. package/dist/worker/console/recovery-cta.js +14 -14
  19. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_lmJjOm.js → abnfDiagram-N423BO3Z-Brn2sPzL.js} +1 -1
  20. package/dist/worker/console/static/assets/{arc-pymM4Qeh.js → arc-BIMNTG0W.js} +1 -1
  21. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw9EPAhi.js → architectureDiagram-T3A2C74G--WtcgR_A.js} +1 -1
  22. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C7_oXJp2.js → blockDiagram-VBNYF7ZC-BT_Ztog1.js} +1 -1
  23. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-LnLWDbh8.js → c4Diagram-5PPSVZJV-C5hbVr-l.js} +1 -1
  24. package/dist/worker/console/static/assets/channel-DNWB4hpR.js +1 -0
  25. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BBiu3sCW.js → chunk-2GRJ4B5K-BKKnyR9Z.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DiXqDQml.js → chunk-2Q5K7J3B-BHs6biqA.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-lmNq9nYk.js → chunk-5RXB4S5H-BdSFy_vP.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-boBu74dU.js → chunk-5VM5RSS4-BpJIdTKV.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CjxqoJLQ.js → chunk-6Q2QTUOP-C_iWJhkT.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DL0vQ9MG.js → chunk-GF5L2VYU-BTbmx3-k.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DGPqE-O0.js → chunk-JWPE2WC7-D4Ee6DmP.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-v4BbT_r2.js → chunk-KBJHAD2P-B487SORR.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Ci0ynvyL.js → chunk-RYQCIY6F-BKYHjNrr.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CGsbJoTV.js → chunk-XXDRQBXY-DrTdoYZG.js} +1 -1
  35. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +1 -0
  36. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +1 -0
  37. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CzoRDEI7.js → cose-bilkent-JH36ORCC-CttUY_Jo.js} +1 -1
  38. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BOwFwbX4.js → cynefin-VYW2F7L2-B4Rm5gnR.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2Cfg451l.js → cynefinDiagram-MW4NZA55-ByxHBq_S.js} +1 -1
  40. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DbT0D6Y5.js → dagre-VZM6K2ZE-DGrfNFA0.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dfd_JoMs.js → diagram-7IWD3JNH-KjrUPquv.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DaGm82kh.js → diagram-B4RE2ZJO-VS4mlm75.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DMoI8Ibc.js → diagram-LBJQPF4R-BLXYmFTM.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtIFA3aY.js → diagram-Q27KOJAE-C6xoFAqH.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-UB23O5K3-D_2X1DTZ.js → diagram-UB23O5K3-C478dCe7.js} +1 -1
  46. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-yFW5ZYlZ.js → ebnfDiagram-BXEA7PRR-DHEpW5zN.js} +1 -1
  47. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DBXh5Svj.js → erDiagram-JOGREHBK-CA5GKfo9.js} +1 -1
  48. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C4ycm8KM.js → flowDiagram-UKHOOZJN-zysi0Fl-.js} +1 -1
  49. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-qr-_Pekr.js → ganttDiagram-PKOTCBZU-DGzcKA8Y.js} +1 -1
  50. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DzOSLv_A.js → gitGraphDiagram-DS77QQ5N-CHsuIG1w.js} +1 -1
  51. package/dist/worker/console/static/assets/index-CJh6pVLm.js +482 -0
  52. package/dist/worker/console/static/assets/index-C_kvKmFM.css +1 -0
  53. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C7Hl8D7l.js → infoDiagram-6WML65LV-BexZHPCp.js} +1 -1
  54. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-fBxVlr6t.js → ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js} +1 -1
  55. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CoiJEVCl.js → journeyDiagram-NVQOT4AX-XDW3qrJ7.js} +1 -1
  56. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-TJXpxNuV.js → kanban-definition-27J2QSJJ-DJ07eEaN.js} +1 -1
  57. package/dist/worker/console/static/assets/{linear-CjQWz1pG.js → linear-DL-zeqOn.js} +1 -1
  58. package/dist/worker/console/static/assets/{mermaid.core-DY9_apIE.js → mermaid.core-C0HdXtOf.js} +5 -5
  59. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-HB0Id8gM.js → mindmap-definition-FAOFIHXS-CqRX0lrt.js} +1 -1
  60. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-0OSWgr6v.js → pegDiagram-VL7TDLO6-BVM9JZ2H.js} +1 -1
  61. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cei1GGC2.js → pieDiagram-7S7Q4E2Y-BA3ozdKk.js} +1 -1
  62. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CDy0Adfu.js → quadrantDiagram-CIZ2JOQS-B-0_p9_4.js} +1 -1
  63. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwqzUWwN.js → railroadDiagram-AXF67PYL-DHRzPWFc.js} +1 -1
  64. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bif2IBRL.js → requirementDiagram-LRYGKXZP-9BEC_PcW.js} +1 -1
  65. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CYIoNMLg.js → sankeyDiagram-W5VNT64P-DMxeW4rN.js} +1 -1
  66. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BDS7Y0aQ.js → sequenceDiagram-SI44F4Z6-DixP6LIU.js} +1 -1
  67. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-lZic-_Bp.js → sizeCapture-X5ZJPWSS-B_EifJBo.js} +1 -1
  68. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-WCVlCqSd.js → stateDiagram-OKZ733FA-BwikwWbX.js} +1 -1
  69. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +1 -0
  70. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-pGccxTw_.js → swimlanes-SLNWSIFB-C162l9o1.js} +2 -2
  71. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.js +8 -0
  72. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D29RlrQA.js → timeline-definition-Z64GVDOM-mpe_u1i4.js} +1 -1
  73. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-01fEM1NU.js → vennDiagram-T6HMQDX7-DyUk54HX.js} +1 -1
  74. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C4GeB6dq.js → wardleyDiagram-T6FBY63Y-HYyMkzfm.js} +1 -1
  75. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BXHrXHps.js → xychartDiagram-ELKLHX3M-DRoh7skR.js} +1 -1
  76. package/dist/worker/console/static/index.html +2 -2
  77. package/dist/worker/console/static-src/app/console-types.js +9 -1
  78. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  79. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  80. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  81. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +4 -2
  82. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  83. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +8 -1
  84. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  85. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  86. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  87. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  88. package/dist/worker/console/workspace-initialization.js +3 -3
  89. package/dist/worker/observability/read-model.js +23 -2
  90. package/dist/worker/observe/node-context-insights.js +479 -0
  91. package/docs/README.md +1 -1
  92. package/docs/templates/backend-test-dag.json +1 -0
  93. package/package.json +4 -4
  94. package/dist/worker/console/static/assets/channel-Ym13M3Lb.js +0 -1
  95. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeFR1wGo.js +0 -1
  96. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeFR1wGo.js +0 -1
  97. package/dist/worker/console/static/assets/index-PFjB6g6l.js +0 -482
  98. package/dist/worker/console/static/assets/index-ZtyB6Ol-.css +0 -1
  99. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-cj5UUB6m.js +0 -1
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-C_Qp6bnh.js +0 -8
@@ -296,6 +296,14 @@ export function createWizardLocalHeadersExtension(options) {
296
296
  }
297
297
  export { THINKING_LEVELS, RuntimeSnapshotUnavailableError, projectRuntimeSnapshot, };
298
298
  export { createDefaultPiSdkBindings } from "./pi-runtime/sdk-bindings.js";
299
+ /** Catalog listing must not load skills/extensions/context — those belong to reopen. */
300
+ const CATALOG_RESOURCE_LOADER_OPTIONS = Object.freeze({
301
+ noContextFiles: true,
302
+ noSkills: true,
303
+ noExtensions: true,
304
+ noPromptTemplates: true,
305
+ noThemes: true,
306
+ });
299
307
  export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
300
308
  export function buildRequestContextEvent(input) {
301
309
  const systemPrompt = input.session.state?.systemPrompt ?? input.session.systemPrompt ?? "";
@@ -440,6 +448,8 @@ export class ConsolePiRuntime {
440
448
  mainlineLeaves = new Map();
441
449
  /** Per-sessionId single-flight materialization (create fast-path / readiness fence). */
442
450
  sessionInit = new Map();
451
+ /** Concurrent reopen of the same historical JSONL shares one in-flight promise. */
452
+ reopenInFlight = new Map();
443
453
  /** Resolved model binding after materialization (create may return before this is known). */
444
454
  sessionModels = new Map();
445
455
  /** Durable session mode; record is the source of truth, this is the live handle cache. */
@@ -1251,11 +1261,68 @@ export class ConsolePiRuntime {
1251
1261
  entries: manager.getBranch?.() ?? [],
1252
1262
  };
1253
1263
  }
1264
+ /**
1265
+ * Workspace-scoped model catalog. A live session's ModelRuntime is reused
1266
+ * when present; otherwise services are created without opening the Pi
1267
+ * conversation JSONL so the picker is not blocked on cold reopen.
1268
+ */
1269
+ workspaceModelRuntime;
1270
+ workspaceInspection;
1271
+ async getWorkspaceModelRuntime() {
1272
+ if (!this.workspaceModelRuntime) {
1273
+ this.workspaceModelRuntime = (async () => {
1274
+ const agentDir = await safeGetAgentDir(this.bindings);
1275
+ const { services } = await this.bindings.createServices({
1276
+ cwd: this.options.cwd,
1277
+ agentDir,
1278
+ resourceLoaderOptions: { ...CATALOG_RESOURCE_LOADER_OPTIONS },
1279
+ });
1280
+ return services.modelRuntime;
1281
+ })().catch((error) => {
1282
+ this.workspaceModelRuntime = undefined;
1283
+ throw error;
1284
+ });
1285
+ }
1286
+ return this.workspaceModelRuntime;
1287
+ }
1288
+ /**
1289
+ * Full OPEN-surface services for inspect UIs. No SessionManager / JSONL.
1290
+ * Cached without sessionId so todo/goal extensions stay off the shared scope.
1291
+ */
1292
+ async getWorkspaceInspectionServices() {
1293
+ if (!this.workspaceInspection) {
1294
+ this.workspaceInspection = (async () => {
1295
+ const agentDir = await safeGetAgentDir(this.bindings);
1296
+ const appendSystemPrompt = composeChatSystemPromptParts({
1297
+ sessionMode: DEFAULT_CONSOLE_CHAT_SESSION_MODE,
1298
+ workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1299
+ });
1300
+ const { services } = await this.bindings.createServices({
1301
+ cwd: this.options.cwd,
1302
+ agentDir,
1303
+ resourceLoaderOptions: await this.buildResourceLoaderOptions({
1304
+ appendSystemPrompt,
1305
+ }),
1306
+ });
1307
+ return {
1308
+ modelRuntime: services.modelRuntime,
1309
+ resourceLoader: services
1310
+ .resourceLoader,
1311
+ settingsManager: services.settingsManager,
1312
+ };
1313
+ })().catch((error) => {
1314
+ this.workspaceInspection = undefined;
1315
+ throw error;
1316
+ });
1317
+ }
1318
+ return this.workspaceInspection;
1319
+ }
1254
1320
  async listModels(sessionId) {
1255
- await this.ensureSessionReady(sessionId);
1256
- const modelRuntime = this.modelRuntimes.get(sessionId);
1257
- if (!this.sessions.has(sessionId) || !modelRuntime)
1258
- throw new Error(`chat session not active: ${sessionId}`);
1321
+ const live = this.modelRuntimes.get(sessionId);
1322
+ if (live && this.sessions.has(sessionId)) {
1323
+ return this.bindings.listAvailableModels({ modelRuntime: live });
1324
+ }
1325
+ const modelRuntime = await this.getWorkspaceModelRuntime();
1259
1326
  return this.bindings.listAvailableModels({ modelRuntime });
1260
1327
  }
1261
1328
  async applyModel(sessionId, model) {
@@ -1518,11 +1585,11 @@ export class ConsolePiRuntime {
1518
1585
  return { ok: true, revision: this.revisions.get(sessionId) ?? 0 };
1519
1586
  }
1520
1587
  /**
1521
- * Project the unified live runtime snapshot (design §2.2). The current Pi
1522
- * session is the single source of truth: real system prompt, real model /
1523
- * thinking selection (auto + effective), the session modelRuntime model
1524
- * list, and the session ResourceLoader inventories. Undefined when the
1525
- * session is not active — callers fail closed instead of fabricating data.
1588
+ * Project the unified runtime snapshot (design §2.2). A live Pi session is
1589
+ * the source of truth when present. Inspect GETs may pass
1590
+ * `workspaceProjection` to render ResourceLoader inventories without
1591
+ * opening the conversation JSONL. Unknown ids without that hint stay
1592
+ * undefined so callers fail closed.
1526
1593
  */
1527
1594
  /**
1528
1595
  * ConsolePiRuntime.listSlashCommands
@@ -1541,18 +1608,27 @@ export class ConsolePiRuntime {
1541
1608
  return collectSlashCommands({ session, resourceLoader });
1542
1609
  }
1543
1610
  async getRuntimeSnapshot(sessionId, options) {
1544
- try {
1545
- await this.ensureSessionReady(sessionId);
1546
- }
1547
- catch (error) {
1548
- if (error instanceof PiSessionInitFailedError)
1549
- throw error;
1550
- // Inactive / unknown session: fail closed with undefined (no fabricated snapshot).
1551
- return undefined;
1611
+ const initializing = this.sessionInit.get(sessionId)?.status === "initializing";
1612
+ if (this.sessions.has(sessionId) || initializing) {
1613
+ try {
1614
+ await this.ensureSessionReady(sessionId);
1615
+ }
1616
+ catch (error) {
1617
+ if (error instanceof PiSessionInitFailedError)
1618
+ throw error;
1619
+ if (options?.workspaceProjection) {
1620
+ return this.projectWorkspaceRuntimeSnapshot(sessionId, options);
1621
+ }
1622
+ return undefined;
1623
+ }
1552
1624
  }
1553
1625
  const session = this.sessions.get(sessionId);
1554
- if (!session)
1626
+ if (!session) {
1627
+ if (options?.workspaceProjection) {
1628
+ return this.projectWorkspaceRuntimeSnapshot(sessionId, options);
1629
+ }
1555
1630
  return undefined;
1631
+ }
1556
1632
  const services = this.serviceScopes.get(sessionId);
1557
1633
  const modelRuntime = this.modelRuntimes.get(sessionId);
1558
1634
  const resourceLoader = services?.resourceLoader;
@@ -1653,6 +1729,116 @@ export class ConsolePiRuntime {
1653
1729
  hasBash: true,
1654
1730
  });
1655
1731
  }
1732
+ resolveWorkspaceThinkingSelection(sessionId, stored) {
1733
+ const live = this.thinkingSelections.get(sessionId);
1734
+ if (live)
1735
+ return live;
1736
+ if (!stored || stored === "auto")
1737
+ return "auto";
1738
+ if (THINKING_LEVELS.includes(stored)) {
1739
+ return stored;
1740
+ }
1741
+ return "auto";
1742
+ }
1743
+ /**
1744
+ * Inspect projection without SessionManager.open. Used only when the Agent
1745
+ * is not live and the caller confirmed a durable session record.
1746
+ */
1747
+ async projectWorkspaceRuntimeSnapshot(sessionId, options) {
1748
+ const hint = options.workspaceProjection ?? {};
1749
+ const sessionMode = hint.sessionMode ?? this.sessionModeOf(sessionId);
1750
+ let inspection;
1751
+ try {
1752
+ inspection = await this.getWorkspaceInspectionServices();
1753
+ }
1754
+ catch (error) {
1755
+ throw new RuntimeSnapshotUnavailableError(`workspace runtime projection failed: ${error instanceof Error ? error.message : String(error)}`);
1756
+ }
1757
+ const resourceLoader = inspection.resourceLoader;
1758
+ const settingsManager = inspection.settingsManager;
1759
+ const thinkingSelection = this.resolveWorkspaceThinkingSelection(sessionId, hint.thinkingLevel);
1760
+ const effectiveThinkingLevel = settingsManager?.getDefaultThinkingLevel?.() ??
1761
+ (thinkingSelection === "auto" ? undefined : thinkingSelection);
1762
+ const rawSystemPrompt = resourceLoader?.getSystemPrompt?.() ||
1763
+ composeChatSystemPrompt({
1764
+ sessionMode,
1765
+ workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1766
+ });
1767
+ if (!rawSystemPrompt) {
1768
+ throw new RuntimeSnapshotUnavailableError("system prompt projection failed: workspace prompt was empty");
1769
+ }
1770
+ let models = [];
1771
+ try {
1772
+ models = this.bindings
1773
+ .listAvailableModels({ modelRuntime: inspection.modelRuntime })
1774
+ .map((entry) => ({
1775
+ provider: entry.provider,
1776
+ id: entry.id,
1777
+ ...(entry.name ? { name: entry.name } : {}),
1778
+ hasCredentials: entry.hasCredentials,
1779
+ }));
1780
+ }
1781
+ catch (error) {
1782
+ throw new RuntimeSnapshotUnavailableError(`model enumeration failed: ${error instanceof Error ? error.message : String(error)}`);
1783
+ }
1784
+ const thinkingLevels = [
1785
+ { value: "auto" },
1786
+ ...THINKING_LEVELS.map((value) => ({ value })),
1787
+ ];
1788
+ let skillPrefs = { schemaVersion: 1, skills: {} };
1789
+ let skillPrefsError;
1790
+ if (resourceLoader) {
1791
+ try {
1792
+ skillPrefs = await this.loadSkillAutoInvocationPreferences();
1793
+ }
1794
+ catch (error) {
1795
+ skillPrefsError = error;
1796
+ }
1797
+ }
1798
+ const activeTools = chatActiveToolBaseline(sessionMode);
1799
+ const inventories = await collectRuntimeInventories({
1800
+ resourceLoader,
1801
+ skillPrefs,
1802
+ skillPrefsError,
1803
+ agentDir: await safeGetAgentDir(this.bindings),
1804
+ cwd: this.options.cwd,
1805
+ tools: [],
1806
+ activeToolNames: activeTools,
1807
+ });
1808
+ const storedModel = this.sessionModels.get(sessionId);
1809
+ const provider = storedModel?.provider ?? hint.modelProvider;
1810
+ const modelId = storedModel?.modelId ?? hint.modelId;
1811
+ return projectRuntimeSnapshot({
1812
+ sessionId,
1813
+ revision: this.revisions.get(sessionId) ?? 0,
1814
+ loadedAt: new Date().toISOString(),
1815
+ selection: {
1816
+ ...(provider && modelId
1817
+ ? { model: { provider, id: modelId } }
1818
+ : {}),
1819
+ thinkingSelection,
1820
+ ...(effectiveThinkingLevel
1821
+ ? { effectiveThinkingLevel: effectiveThinkingLevel }
1822
+ : {}),
1823
+ },
1824
+ models,
1825
+ thinkingLevels,
1826
+ systemPrompt: {
1827
+ ...(options.includeRaw ? { raw: rawSystemPrompt } : {}),
1828
+ text: rawSystemPrompt,
1829
+ sha256: createHash("sha256").update(rawSystemPrompt).digest("hex"),
1830
+ characters: rawSystemPrompt.length,
1831
+ },
1832
+ activeTools,
1833
+ contextFiles: inventories.contextFiles,
1834
+ skills: inventories.skills,
1835
+ extensions: inventories.extensions,
1836
+ packages: inventories.packages,
1837
+ mcp: inventories.mcp,
1838
+ diagnostics: inventories.diagnostics,
1839
+ hasBash: true,
1840
+ });
1841
+ }
1656
1842
  /**
1657
1843
  * Re-open a previously persisted Chat session after Console restart or
1658
1844
  * browser refresh (roadmap M1-S04 / S05).
@@ -1667,6 +1853,19 @@ export class ConsolePiRuntime {
1667
1853
  * than silently creating a fresh empty session and losing prior context.
1668
1854
  */
1669
1855
  async reopenSession(init) {
1856
+ const sessionId = init.sessionId;
1857
+ const existing = this.reopenInFlight.get(sessionId);
1858
+ if (existing)
1859
+ return existing;
1860
+ const pending = this.reopenSessionExclusive(init).finally(() => {
1861
+ if (this.reopenInFlight.get(sessionId) === pending) {
1862
+ this.reopenInFlight.delete(sessionId);
1863
+ }
1864
+ });
1865
+ this.reopenInFlight.set(sessionId, pending);
1866
+ return pending;
1867
+ }
1868
+ async reopenSessionExclusive(init) {
1670
1869
  await this.syncAimaxAuthToken();
1671
1870
  const sessionId = init.sessionId;
1672
1871
  const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
3
  import { lstat, open, readdir } from "node:fs/promises";
4
+ import { sameFileIdentity } from "../../../shared/file-identity.js";
4
5
  import path from "node:path";
5
6
  import { guardReadPath, isSensitivePath, scrubSecrets, } from "./explore-tools.js";
6
7
  const TREE_LIMIT = 500;
@@ -126,6 +127,55 @@ export async function listRepoDirectory(repoRoot, rawPath) {
126
127
  throw error;
127
128
  }
128
129
  }
130
+ /** Composer `@` palette keeps the first page of a directory listing scannable. */
131
+ export const FILE_PALETTE_LIMIT = 50;
132
+ /**
133
+ * Split an `@` token query into a directory listing target and basename filter.
134
+ * Empty or slash-free queries stay on the workspace root (M3 fuzzy is out of
135
+ * scope). A trailing slash lists that directory with an empty fragment.
136
+ */
137
+ export function parseFilePaletteQuery(rawQuery) {
138
+ const query = rawQuery.replaceAll("\\", "/").replace(/^\/+/u, "");
139
+ const slash = query.lastIndexOf("/");
140
+ if (slash < 0)
141
+ return { directory: "", fragment: query };
142
+ return {
143
+ directory: query.slice(0, slash),
144
+ fragment: query.slice(slash + 1),
145
+ };
146
+ }
147
+ export function projectFilePaletteEntries(entries, fragment) {
148
+ const showDots = fragment.startsWith(".");
149
+ const needle = fragment.toLowerCase();
150
+ const projected = [];
151
+ for (const entry of entries) {
152
+ if (!showDots && entry.name.startsWith("."))
153
+ continue;
154
+ if (needle !== "" && !entry.name.toLowerCase().includes(needle))
155
+ continue;
156
+ const isDir = entry.kind === "directory";
157
+ projected.push({
158
+ path: isDir ? `${entry.path}/` : entry.path,
159
+ isDir,
160
+ });
161
+ if (projected.length >= FILE_PALETTE_LIMIT)
162
+ break;
163
+ }
164
+ return projected;
165
+ }
166
+ /** One-level `@` file palette listing. Unreadable or out-of-repo queries are empty. */
167
+ export async function listChatFilePalette(repoRoot, rawQuery) {
168
+ const { directory, fragment } = parseFilePaletteQuery(rawQuery);
169
+ try {
170
+ const listed = await listRepoDirectory(repoRoot, directory);
171
+ return projectFilePaletteEntries(listed.entries, fragment);
172
+ }
173
+ catch (error) {
174
+ if (error instanceof RepoBrowserError)
175
+ return [];
176
+ throw error;
177
+ }
178
+ }
129
179
  export async function readRepoPreview(repoRoot, rawPath) {
130
180
  const guard = await guardReadPath(repoRoot, rawPath);
131
181
  if (!guard.ok)
@@ -143,8 +193,7 @@ export async function readRepoPreview(repoRoot, rawPath) {
143
193
  throw new RepoBrowserError(403, guardedCode(rechecked.code), "file path changed while it was being opened");
144
194
  const current = await lstat(rechecked.abs);
145
195
  if (current.isSymbolicLink() ||
146
- current.dev !== info.dev ||
147
- current.ino !== info.ino)
196
+ !sameFileIdentity(info, current))
148
197
  throw new RepoBrowserError(403, "OUT_OF_REPO", "file identity changed while it was being opened");
149
198
  size = info.size;
150
199
  const readSize = Math.min(size, TEXT_PREVIEW_LIMIT);
@@ -204,8 +253,7 @@ export async function readRepoHtmlAsset(repoRoot, rawPath) {
204
253
  throw new RepoBrowserError(403, guardedCode(rechecked.code), "file path changed while it was being opened");
205
254
  const current = await lstat(rechecked.abs);
206
255
  if (current.isSymbolicLink() ||
207
- current.dev !== info.dev ||
208
- current.ino !== info.ino)
256
+ !sameFileIdentity(info, current))
209
257
  throw new RepoBrowserError(403, "OUT_OF_REPO", "file identity changed while it was being opened");
210
258
  return {
211
259
  body: await handle.readFile(),
@@ -24,9 +24,8 @@ import { projectDeferredTurnSummary, projectDeferredTurnSummaries, } from "./def
24
24
  import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
25
25
  import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
26
26
  import { ComposerDraftStore } from "./composer-draft-store.js";
27
- import { walkRepoFiles } from "./repo-walk.js";
28
- import { isSensitivePath, scrubSecrets } from "./explore-tools.js";
29
- import { listRepoDirectory, matchRepoHtmlPreviewRoute, readRepoHtmlAsset, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
27
+ import { scrubSecrets } from "./explore-tools.js";
28
+ import { listChatFilePalette, listRepoDirectory, matchRepoHtmlPreviewRoute, readRepoHtmlAsset, readRepoPreview, RepoBrowserError, } from "./repo-browser.js";
30
29
  import { projectRuntimeContext } from "./runtime-context.js";
31
30
  import { patchModelsConfig, readModelsConfig, readPackageInventory, } from "./pi-console-config.js";
32
31
  import { ChatInterviewAdapter, projectChatInterviewState, } from "./interview-adapter.js";
@@ -36,6 +35,7 @@ import { dispatchOperatorAction } from "../operator-actions.js";
36
35
  import { makeHumanGateCard, } from "./human-gate-card.js";
37
36
  import { ContractApplyReceiptStore } from "./contract-apply-receipt-store.js";
38
37
  import { projectTaskContext } from "./context-panel.js";
38
+ import { deriveContextInsights, estimateTokensFromText, } from "./context-insights.js";
39
39
  import { deriveChatSessionTelemetry, deriveChatTurnTelemetry } from "./session-stats.js";
40
40
  import { contractApplyPayloadHash, issueHumanGateToken, verifyHumanGateToken, } from "../human-gate-token.js";
41
41
  import { sendJson } from "../routes.js";
@@ -65,6 +65,14 @@ function reopenInitFromRecord(record) {
65
65
  sessionMode: record.sessionMode,
66
66
  };
67
67
  }
68
+ function workspaceProjectionFromRecord(record) {
69
+ return {
70
+ sessionMode: record.sessionMode,
71
+ ...(record.modelProvider ? { modelProvider: record.modelProvider } : {}),
72
+ ...(record.modelId ? { modelId: record.modelId } : {}),
73
+ ...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel } : {}),
74
+ };
75
+ }
68
76
  async function readChatSessionMode(deps, sessionId) {
69
77
  let record;
70
78
  try {
@@ -3020,28 +3028,8 @@ async function handlePiConfig(req, res, deps, resource) {
3020
3028
  async function handleChatFiles(res, deps, sessionId, query) {
3021
3029
  if (!(await requireChatSession(res, deps, sessionId)))
3022
3030
  return;
3023
- const q = query.trim().toLowerCase();
3024
- const walked = await walkRepoFiles(deps.appData.repoRoot, {
3025
- skipSensitive: true,
3026
- maxFiles: 5000,
3027
- includeDirs: true,
3028
- });
3029
- const entries = walked
3030
- .filter((entry) => !isSensitivePath(entry.repoRelative) &&
3031
- (!q || entry.repoRelative.toLowerCase().includes(q)))
3032
- .sort((a, b) => {
3033
- const aDir = a.repoRelative.endsWith("/");
3034
- const bDir = b.repoRelative.endsWith("/");
3035
- if (aDir !== bDir)
3036
- return aDir ? -1 : 1;
3037
- return a.repoRelative.localeCompare(b.repoRelative);
3038
- })
3039
- .slice(0, 50)
3040
- .map((entry) => ({
3041
- path: entry.repoRelative,
3042
- isDir: entry.repoRelative.endsWith("/"),
3043
- }));
3044
- sendJson(res, 200, { ok: true, files: entries });
3031
+ const files = await listChatFilePalette(deps.appData.repoRoot, query);
3032
+ sendJson(res, 200, { ok: true, files });
3045
3033
  }
3046
3034
  export async function handleChatCommands(res, deps, sessionId) {
3047
3035
  try {
@@ -3145,20 +3133,25 @@ async function handleSddBingo(req, res, deps, sessionId) {
3145
3133
  }
3146
3134
  }
3147
3135
  export async function handleRuntimeContext(res, deps, sessionId) {
3148
- const ensured = await ensureActiveSession(deps, sessionId);
3149
- if (!ensured.ok) {
3150
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3136
+ const record = await deps.store.get(sessionId);
3137
+ if (!record) {
3138
+ sendJson(res, 404, {
3139
+ ok: false,
3140
+ error: {
3141
+ code: "PI_SESSION_NOT_FOUND",
3142
+ message: `chat session not found: ${sessionId}`,
3143
+ },
3144
+ });
3151
3145
  return;
3152
3146
  }
3153
- // AC-03: system prompt comes from the REAL Pi session (agent.state.systemPrompt
3154
- // fallback chain); resources come from that session's ResourceLoader. The raw
3155
- // prompt stays server-side; the browser only ever receives the scrubbed +
3156
- // length-limited projection. RF-02: a critical projection failure is 503
3157
- // PI_RUNTIME_UNAVAILABLE — never a 500 leak or a fake-empty context.
3147
+ // Live Agent is preferred. Cold inspect uses workspace ResourceLoader /
3148
+ // ModelRuntime without SessionManager.open(jsonl); prompt still reopens.
3149
+ // RF-02: a critical projection failure is 503 PI_RUNTIME_UNAVAILABLE.
3158
3150
  let snapshot;
3159
3151
  try {
3160
3152
  snapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3161
3153
  includeRaw: true,
3154
+ workspaceProjection: workspaceProjectionFromRecord(record),
3162
3155
  });
3163
3156
  }
3164
3157
  catch (error) {
@@ -3208,19 +3201,94 @@ export async function handleRuntimeContext(res, deps, sessionId) {
3208
3201
  }),
3209
3202
  });
3210
3203
  }
3204
+ /**
3205
+ * GET /sessions/:id/context-insights: dsh-context parity dashboard projection.
3206
+ * Folds the persisted event ring (works for idle/restored sessions) and, when
3207
+ * the Pi session is live, anchors the estimate with the runtime snapshot's
3208
+ * system-prompt size + active tool count. Read-only; never fabricates payload
3209
+ * text beyond the bounded previews already stored on the ring.
3210
+ */
3211
+ export async function handleContextInsights(res, deps, sessionId) {
3212
+ const record = await deps.store.get(sessionId);
3213
+ if (!record) {
3214
+ sendJson(res, 404, {
3215
+ ok: false,
3216
+ error: { code: "NOT_FOUND", message: `chat session not found: ${sessionId}` },
3217
+ });
3218
+ return;
3219
+ }
3220
+ await deps.operationLinker.recoverSession(sessionId);
3221
+ const snapshot = deps.events.snapshot(sessionId);
3222
+ let systemPromptChars;
3223
+ let systemPromptPreview;
3224
+ let activeTools;
3225
+ let skillInjections;
3226
+ try {
3227
+ const runtimeSnapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3228
+ includeRaw: true,
3229
+ workspaceProjection: workspaceProjectionFromRecord(record),
3230
+ });
3231
+ if (runtimeSnapshot) {
3232
+ systemPromptChars = runtimeSnapshot.systemPrompt.characters;
3233
+ const raw = runtimeSnapshot.systemPrompt.raw ??
3234
+ runtimeSnapshot.systemPrompt.text;
3235
+ if (typeof raw === "string" && raw) {
3236
+ systemPromptPreview = raw.slice(0, 120_000);
3237
+ }
3238
+ activeTools = runtimeSnapshot.activeTools;
3239
+ skillInjections = runtimeSnapshot.skills
3240
+ .slice(0, 50)
3241
+ .map((skill) => ({
3242
+ name: skill.name,
3243
+ tokens: estimateTokensFromText(skill.description ?? skill.name),
3244
+ }));
3245
+ }
3246
+ }
3247
+ catch {
3248
+ // Insights are derived from the durable ring; a live-snapshot miss only
3249
+ // drops the anchor figures / browser static categories, never the dashboard.
3250
+ }
3251
+ // Tool schemas: capabilities registry projection for whitelisted operator
3252
+ // actions, plus Pi builtins active on this session.
3253
+ const schemaTools = buildModelCallableToolSchemas().map((tool) => ({
3254
+ id: tool.toolId,
3255
+ description: tool.description,
3256
+ }));
3257
+ const activeSet = new Set((activeTools ?? []).map((t) => t.toLowerCase()));
3258
+ const builtinTools = (activeTools ?? [])
3259
+ .filter((name) => !schemaTools.some((t) => t.id === name))
3260
+ .map((name) => ({ id: name, description: "" }));
3261
+ const insights = deriveContextInsights(snapshot, {
3262
+ sessionId,
3263
+ ...(systemPromptChars !== undefined ? { systemPromptChars } : {}),
3264
+ ...(systemPromptPreview !== undefined
3265
+ ? { systemPromptPreview }
3266
+ : {}),
3267
+ tools: [...schemaTools, ...builtinTools],
3268
+ ...(skillInjections ? { skillInjections } : {}),
3269
+ });
3270
+ sendJson(res, 200, { ok: true, insights });
3271
+ }
3211
3272
  // Exported for the direct handler-boundary tests (trace binding for
3212
3273
  // vt-route-model-errors); router dispatch behavior is unchanged.
3213
3274
  export async function handleModels(req, res, deps, sessionId) {
3214
- // AC-01/AC-06: models (and model/thinking mutation) requests reuse the same
3215
- // activation path. A missing/reopen-failed runtime is a STRUCTURED error —
3216
- // never a fabricated empty model list.
3217
- const ensured = await ensureActiveSession(deps, sessionId);
3218
- if (!ensured.ok) {
3219
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3220
- return;
3221
- }
3222
3275
  try {
3223
3276
  if ((req.method ?? "GET").toUpperCase() === "GET") {
3277
+ // Catalog is workspace-scoped (models.json / ModelRuntime). Do not wait
3278
+ // for Pi conversation reopen and do not start it here — prompt / reload
3279
+ // / model mutation still reopen, and concurrent reopen would stall this
3280
+ // cheap list on the single Node thread.
3281
+ const record = await deps.store.get(sessionId);
3282
+ if (!record) {
3283
+ sendJson(res, 404, {
3284
+ ok: false,
3285
+ error: {
3286
+ code: "PI_SESSION_NOT_FOUND",
3287
+ message: `chat session not found: ${sessionId}`,
3288
+ },
3289
+ });
3290
+ return;
3291
+ }
3224
3292
  let models;
3225
3293
  try {
3226
3294
  models = await deps.runtime.listModels(sessionId);
@@ -3249,6 +3317,11 @@ export async function handleModels(req, res, deps, sessionId) {
3249
3317
  });
3250
3318
  return;
3251
3319
  }
3320
+ const ensured = await ensureActiveSession(deps, sessionId);
3321
+ if (!ensured.ok) {
3322
+ sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3323
+ return;
3324
+ }
3252
3325
  const gate = gateMutation(req, deps);
3253
3326
  if (!gate.ok) {
3254
3327
  sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
@@ -3304,18 +3377,24 @@ async function requireChatSession(res, deps, sessionId) {
3304
3377
  });
3305
3378
  return false;
3306
3379
  }
3307
- /**
3308
- * Unified session activation (design §2.1 / AC-01): models, runtime snapshot,
3309
- * reload and model/thinking mutation requests all flow through this path.
3310
- *
3311
- * - runtime already holds the session → ok;
3312
- * - only a persisted record exists → reopen from record.sessionFile;
3313
- * - no record / missing session file → 404 PI_SESSION_NOT_FOUND;
3314
- * - SDK reopen failure → 503 PI_RUNTIME_UNAVAILABLE.
3315
- *
3316
- * Never fabricates a fresh session or an empty model list.
3317
- */
3380
+ const ensureActiveSessionInFlight = new WeakMap();
3318
3381
  export async function ensureActiveSession(deps, sessionId) {
3382
+ let bySession = ensureActiveSessionInFlight.get(deps);
3383
+ if (!bySession) {
3384
+ bySession = new Map();
3385
+ ensureActiveSessionInFlight.set(deps, bySession);
3386
+ }
3387
+ const existing = bySession.get(sessionId);
3388
+ if (existing)
3389
+ return existing;
3390
+ const pending = runEnsureActiveSession(deps, sessionId).finally(() => {
3391
+ if (bySession.get(sessionId) === pending)
3392
+ bySession.delete(sessionId);
3393
+ });
3394
+ bySession.set(sessionId, pending);
3395
+ return pending;
3396
+ }
3397
+ async function runEnsureActiveSession(deps, sessionId) {
3319
3398
  // Wait for create-path materialization before treating the session as missing.
3320
3399
  if (typeof deps.runtime.ensureSessionReady === "function") {
3321
3400
  try {
@@ -3561,19 +3640,28 @@ export async function handleReloadSession(req, res, deps, sessionId) {
3561
3640
  });
3562
3641
  }
3563
3642
  /**
3564
- * GET /sessions/:id/runtime-snapshot (AC-01): the unified live snapshot.
3643
+ * GET /sessions/:id/runtime-snapshot (AC-01): live Agent when present,
3644
+ * otherwise workspace ResourceLoader projection (no JSONL reopen).
3565
3645
  */
3566
3646
  export async function handleRuntimeSnapshot(_res, res, deps, sessionId) {
3567
- const ensured = await ensureActiveSession(deps, sessionId);
3568
- if (!ensured.ok) {
3569
- sendJson(res, ensured.status, { ok: false, error: { code: ensured.code, message: ensured.message } });
3647
+ const record = await deps.store.get(sessionId);
3648
+ if (!record) {
3649
+ sendJson(res, 404, {
3650
+ ok: false,
3651
+ error: {
3652
+ code: "PI_SESSION_NOT_FOUND",
3653
+ message: `chat session not found: ${sessionId}`,
3654
+ },
3655
+ });
3570
3656
  return;
3571
3657
  }
3572
3658
  // RF-02: a critical snapshot projection failure is 503 PI_RUNTIME_UNAVAILABLE
3573
3659
  // — never a 200 fake-empty snapshot.
3574
3660
  let snapshot;
3575
3661
  try {
3576
- snapshot = await deps.runtime.getRuntimeSnapshot(sessionId);
3662
+ snapshot = await deps.runtime.getRuntimeSnapshot(sessionId, {
3663
+ workspaceProjection: workspaceProjectionFromRecord(record),
3664
+ });
3577
3665
  }
3578
3666
  catch (error) {
3579
3667
  if (sendRuntimeUnavailable(res, error))
@@ -4727,7 +4815,7 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4727
4815
  await handleSddBingo(req, res, deps, decodeURIComponent(sddBingo[1]));
4728
4816
  return true;
4729
4817
  }
4730
- const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|runtime-snapshot|models|model|reload|commands)$/);
4818
+ const m4Route = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/(draft|files|runtime-context|runtime-snapshot|context-insights|models|model|reload|commands)$/);
4731
4819
  if (m4Route) {
4732
4820
  const id = decodeURIComponent(m4Route[1]);
4733
4821
  const route = m4Route[2];
@@ -4747,6 +4835,10 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
4747
4835
  await handleRuntimeSnapshot(req, res, deps, id);
4748
4836
  return true;
4749
4837
  }
4838
+ if (route === "context-insights" && method === "GET") {
4839
+ await handleContextInsights(res, deps, id);
4840
+ return true;
4841
+ }
4750
4842
  if (route === "models" && method === "GET") {
4751
4843
  await handleModels(req, res, deps, id);
4752
4844
  return true;