@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.
- package/CHANGELOG.md +29 -1
- package/dist/build-stamp.json +3 -3
- package/dist/executors/pi-sdk-executor.js +7 -7
- package/dist/executors/shell-write-guard.js +26 -3
- package/dist/infrastructure/console/operation-store.js +46 -14
- package/dist/shared/file-identity.js +23 -0
- package/dist/shared/git-workspace-status.js +480 -0
- package/dist/shared/path-safety.js +2 -2
- package/dist/shared/wizard-local-telemetry.js +2 -1
- package/dist/worker/console/chat/context-insights.js +723 -0
- package/dist/worker/console/chat/pi-runtime.js +217 -18
- package/dist/worker/console/chat/repo-browser.js +52 -4
- package/dist/worker/console/chat/routes.js +150 -58
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_lmJjOm.js → abnfDiagram-N423BO3Z-Brn2sPzL.js} +1 -1
- package/dist/worker/console/static/assets/{arc-pymM4Qeh.js → arc-BIMNTG0W.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw9EPAhi.js → architectureDiagram-T3A2C74G--WtcgR_A.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C7_oXJp2.js → blockDiagram-VBNYF7ZC-BT_Ztog1.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-LnLWDbh8.js → c4Diagram-5PPSVZJV-C5hbVr-l.js} +1 -1
- package/dist/worker/console/static/assets/channel-DNWB4hpR.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BBiu3sCW.js → chunk-2GRJ4B5K-BKKnyR9Z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DiXqDQml.js → chunk-2Q5K7J3B-BHs6biqA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-lmNq9nYk.js → chunk-5RXB4S5H-BdSFy_vP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-boBu74dU.js → chunk-5VM5RSS4-BpJIdTKV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CjxqoJLQ.js → chunk-6Q2QTUOP-C_iWJhkT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DL0vQ9MG.js → chunk-GF5L2VYU-BTbmx3-k.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DGPqE-O0.js → chunk-JWPE2WC7-D4Ee6DmP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-v4BbT_r2.js → chunk-KBJHAD2P-B487SORR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Ci0ynvyL.js → chunk-RYQCIY6F-BKYHjNrr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CGsbJoTV.js → chunk-XXDRQBXY-DrTdoYZG.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CzoRDEI7.js → cose-bilkent-JH36ORCC-CttUY_Jo.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BOwFwbX4.js → cynefin-VYW2F7L2-B4Rm5gnR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2Cfg451l.js → cynefinDiagram-MW4NZA55-ByxHBq_S.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DbT0D6Y5.js → dagre-VZM6K2ZE-DGrfNFA0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dfd_JoMs.js → diagram-7IWD3JNH-KjrUPquv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DaGm82kh.js → diagram-B4RE2ZJO-VS4mlm75.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DMoI8Ibc.js → diagram-LBJQPF4R-BLXYmFTM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtIFA3aY.js → diagram-Q27KOJAE-C6xoFAqH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-D_2X1DTZ.js → diagram-UB23O5K3-C478dCe7.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-yFW5ZYlZ.js → ebnfDiagram-BXEA7PRR-DHEpW5zN.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DBXh5Svj.js → erDiagram-JOGREHBK-CA5GKfo9.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C4ycm8KM.js → flowDiagram-UKHOOZJN-zysi0Fl-.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-qr-_Pekr.js → ganttDiagram-PKOTCBZU-DGzcKA8Y.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DzOSLv_A.js → gitGraphDiagram-DS77QQ5N-CHsuIG1w.js} +1 -1
- package/dist/worker/console/static/assets/index-CJh6pVLm.js +482 -0
- package/dist/worker/console/static/assets/index-C_kvKmFM.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C7Hl8D7l.js → infoDiagram-6WML65LV-BexZHPCp.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-fBxVlr6t.js → ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CoiJEVCl.js → journeyDiagram-NVQOT4AX-XDW3qrJ7.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-TJXpxNuV.js → kanban-definition-27J2QSJJ-DJ07eEaN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CjQWz1pG.js → linear-DL-zeqOn.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DY9_apIE.js → mermaid.core-C0HdXtOf.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-HB0Id8gM.js → mindmap-definition-FAOFIHXS-CqRX0lrt.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-0OSWgr6v.js → pegDiagram-VL7TDLO6-BVM9JZ2H.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cei1GGC2.js → pieDiagram-7S7Q4E2Y-BA3ozdKk.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CDy0Adfu.js → quadrantDiagram-CIZ2JOQS-B-0_p9_4.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwqzUWwN.js → railroadDiagram-AXF67PYL-DHRzPWFc.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bif2IBRL.js → requirementDiagram-LRYGKXZP-9BEC_PcW.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CYIoNMLg.js → sankeyDiagram-W5VNT64P-DMxeW4rN.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BDS7Y0aQ.js → sequenceDiagram-SI44F4Z6-DixP6LIU.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-lZic-_Bp.js → sizeCapture-X5ZJPWSS-B_EifJBo.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-WCVlCqSd.js → stateDiagram-OKZ733FA-BwikwWbX.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-pGccxTw_.js → swimlanes-SLNWSIFB-C162l9o1.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D29RlrQA.js → timeline-definition-Z64GVDOM-mpe_u1i4.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-01fEM1NU.js → vennDiagram-T6HMQDX7-DyUk54HX.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C4GeB6dq.js → wardleyDiagram-T6FBY63Y-HYyMkzfm.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BXHrXHps.js → xychartDiagram-ELKLHX3M-DRoh7skR.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +4 -2
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +8 -1
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/observe/node-context-insights.js +479 -0
- package/docs/README.md +1 -1
- package/docs/templates/backend-test-dag.json +1 -0
- package/package.json +4 -4
- package/dist/worker/console/static/assets/channel-Ym13M3Lb.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeFR1wGo.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeFR1wGo.js +0 -1
- package/dist/worker/console/static/assets/index-PFjB6g6l.js +0 -482
- package/dist/worker/console/static/assets/index-ZtyB6Ol-.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-cj5UUB6m.js +0 -1
- 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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
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
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
28
|
-
import {
|
|
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
|
|
3024
|
-
|
|
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
|
|
3149
|
-
if (!
|
|
3150
|
-
sendJson(res,
|
|
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
|
-
//
|
|
3154
|
-
//
|
|
3155
|
-
//
|
|
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):
|
|
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
|
|
3568
|
-
if (!
|
|
3569
|
-
sendJson(res,
|
|
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;
|