@piagent/platform 1.4.0 → 1.5.1
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 +151 -0
- package/README.md +12 -2
- package/README.vi.md +47 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +13 -0
- package/docs/languages.json +20 -4
- package/docs/mcp-and-tools.md +112 -1
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +42 -0
- package/docs/release-install-policy.md +21 -8
- package/docs/runtime-harness-standard.md +1 -1
- package/docs/security-threat-model.md +1 -1
- package/docs/task-implementation-contract.md +11 -4
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +70 -0
- package/docs/vi/architecture.md +13 -0
- package/docs-site/architecture.html +5 -1
- package/docs-site/benchmark.html +5 -1
- package/docs-site/commands.html +5 -1
- package/docs-site/content/en/index.html +49 -0
- package/docs-site/content/en/runtime.html +13 -0
- package/docs-site/content/index.html +49 -0
- package/docs-site/content/runtime.html +28 -0
- package/docs-site/context-engine.html +5 -1
- package/docs-site/en/architecture.html +5 -1
- package/docs-site/en/benchmark.html +5 -1
- package/docs-site/en/commands.html +5 -1
- package/docs-site/en/context-engine.html +5 -1
- package/docs-site/en/index.html +54 -1
- package/docs-site/en/mcp-approval.html +5 -1
- package/docs-site/en/mcp-auth.html +5 -1
- package/docs-site/en/mcp-servers.html +5 -1
- package/docs-site/en/mcp.html +5 -1
- package/docs-site/en/permissions.html +5 -1
- package/docs-site/en/profiles.html +5 -1
- package/docs-site/en/quickstart.html +5 -1
- package/docs-site/en/runtime.html +18 -1
- package/docs-site/en/scope.html +5 -1
- package/docs-site/en/security.html +5 -1
- package/docs-site/en/team.html +5 -1
- package/docs-site/en/whats-new.html +5 -1
- package/docs-site/en/workflows.html +5 -1
- package/docs-site/index.html +54 -1
- package/docs-site/mcp-approval.html +5 -1
- package/docs-site/mcp-auth.html +5 -1
- package/docs-site/mcp-servers.html +5 -1
- package/docs-site/mcp.html +5 -1
- package/docs-site/permissions.html +5 -1
- package/docs-site/profiles.html +5 -1
- package/docs-site/quickstart.html +5 -1
- package/docs-site/runtime.html +33 -1
- package/docs-site/scope.html +5 -1
- package/docs-site/security.html +5 -1
- package/docs-site/team.html +5 -1
- package/docs-site/whats-new.html +5 -1
- package/docs-site/workflows.html +5 -1
- package/evals/fixtures/piagent-webui/document-workspace-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/document-workspace-v1.valid.json +42 -0
- package/evals/fixtures/piagent-webui/session-command-v1.valid.json +2 -1
- package/package.json +14 -2
- package/packages/piagent-core/README.md +3 -2
- package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
- package/packages/piagent-core/capabilities/runtime-integrity.js +51 -4
- package/packages/piagent-core/extensions/document-intake.ts +6 -3
- package/packages/piagent-core/extensions/piagent-guard.ts +20 -237
- package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
- package/packages/piagent-core/extensions/shell-reach.ts +250 -0
- package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
- package/packages/piagent-core/mcp/mcp-server-catalog.js +135 -5
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +9 -2
- package/packages/piagent-core/runtime/input/attachment-document-command.ts +39 -0
- package/packages/piagent-core/runtime/input/attachment-reservation.ts +36 -0
- package/packages/{piagent-webui/extension → piagent-core/runtime/input}/attachment-store.ts +221 -44
- package/packages/piagent-core/runtime/inspection/document-workspace-projection.ts +276 -0
- package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +20 -5
- package/packages/piagent-core/runtime/inspection/session-identity.ts +20 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +25 -2
- package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +9 -13
- package/packages/piagent-core/runtime/registration/workflow-commands.ts +5 -4
- package/packages/piagent-core/runtime/session/token-economics.js +34 -0
- package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/input-routing.ts +13 -2
- package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
- package/packages/piagent-core/security/sensitive-data.js +7 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-Uouhx1QO.js +29 -0
- package/packages/piagent-webui/dist/client/assets/index-BV-_03yl.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-MBZq8XLV.js +174 -0
- package/packages/piagent-webui/dist/client/index.html +2 -2
- package/packages/piagent-webui/extension/lifecycle-controller.ts +15 -0
- package/packages/piagent-webui/extension/piagent-webui.ts +19 -6
- package/packages/piagent-webui/extension/same-session-bridge.ts +12 -12
- package/packages/piagent-webui/gateway/ai-capability-inspection.ts +53 -0
- package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +23 -3
- package/packages/piagent-webui/gateway/native-project-picker.ts +63 -7
- package/packages/piagent-webui/gateway/session-attachment-registry.ts +165 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +62 -9
- package/packages/piagent-webui/gateway/session-command-controller.ts +47 -14
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +67 -12
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +24 -11
- package/packages/piagent-webui/package.json +3 -1
- package/packages/piagent-webui/server/core-inspection-provider.ts +52 -15
- package/packages/piagent-webui/server/ipc-read-model-client.ts +2 -0
- package/packages/piagent-webui/server/loopback-server.ts +63 -25
- package/packages/piagent-webui/server/read-model-provider.ts +2 -0
- package/packages/piagent-webui/server/read-only-router.ts +6 -0
- package/packages/piagent-webui/server/transcript-projection.ts +58 -2
- package/schemas/piagent-webui/attachment-v1.schema.json +24 -4
- package/schemas/piagent-webui/capabilities-v1.schema.json +2 -2
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/document-workspace-v1.schema.json +155 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +14 -3
- package/schemas/piagent-webui/transcript-v1.schema.json +27 -0
- package/scripts/build-docs-site.mjs +21 -1
- package/scripts/explain-command.mjs +240 -0
- package/scripts/install-global.sh +104 -3
- package/scripts/mcp-manage.mjs +17 -4
- package/scripts/pi-session-stats.sh +10 -2
- package/scripts/piagent-cli.mjs +14 -1
- package/scripts/piagent-dashboard.mjs +55 -2
- package/scripts/setup.sh +69 -33
- package/scripts/traffic-snapshot.mjs +214 -0
- package/scripts/typescript-loader.mjs +20 -4
- package/scripts/uninstall-global.sh +53 -2
- package/scripts/update-global.mjs +33 -1
- package/scripts/verify-local.sh +20 -1
- package/scripts/verify-translation-pairing.mjs +121 -0
- package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +0 -172
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +0 -1
|
@@ -13,6 +13,7 @@ export type PiSessionInfo = {
|
|
|
13
13
|
id: string;
|
|
14
14
|
cwd: string;
|
|
15
15
|
name?: string;
|
|
16
|
+
parentSessionPath?: string;
|
|
16
17
|
created: Date;
|
|
17
18
|
modified: Date;
|
|
18
19
|
messageCount: number;
|
|
@@ -20,6 +21,26 @@ export type PiSessionInfo = {
|
|
|
20
21
|
allMessagesText: string;
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
const DELEGATED_TASK_MARKERS = [
|
|
25
|
+
"Task: You are a delegated subagent running from a fork of the parent session.",
|
|
26
|
+
"Task: You are reviving a previous subagent conversation."
|
|
27
|
+
] as const;
|
|
28
|
+
const INTERNAL_HELPER_NAME = /^(?:subagent(?:-|$)|piagent-(?:planner|worker|scout|reviewer|oracle)(?:-|$))/i;
|
|
29
|
+
|
|
30
|
+
export function isUserConversationSession(info: PiSessionInfo): boolean {
|
|
31
|
+
const segments = path.resolve(info.path).split(path.sep);
|
|
32
|
+
const sessionsIndex = segments.lastIndexOf("sessions");
|
|
33
|
+
if (sessionsIndex >= 0 && segments[sessionsIndex + 1] === "subagent") return false;
|
|
34
|
+
const internalName = INTERNAL_HELPER_NAME.test(String(info.name ?? "").trim());
|
|
35
|
+
const hasParent = typeof info.parentSessionPath === "string" && info.parentSessionPath.length > 0;
|
|
36
|
+
const messages = typeof info.allMessagesText === "string" ? info.allMessagesText : "";
|
|
37
|
+
const internalPrompt = DELEGATED_TASK_MARKERS.some((marker) => messages.includes(marker));
|
|
38
|
+
// User-created forks also carry parentSessionPath, so lineage alone is not enough. A helper identity or the
|
|
39
|
+
// runtime-only delegation prompt plus lineage is high-confidence internal evidence. Keep this classification
|
|
40
|
+
// at the gateway boundary so catalog, read models, commands and live streams all fail closed together.
|
|
41
|
+
return !((internalName && (hasParent || internalPrompt)) || (hasParent && internalPrompt));
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
function catalogRef(key: Buffer, namespace: string, value: string): string {
|
|
24
45
|
return `${namespace}_${createHmac("sha256", key).update(value).digest("base64url").slice(0, 43)}`;
|
|
25
46
|
}
|
|
@@ -33,6 +54,43 @@ function display(value: unknown, maximum: number, fallback: string): string {
|
|
|
33
54
|
return (clean || fallback).slice(0, maximum);
|
|
34
55
|
}
|
|
35
56
|
|
|
57
|
+
const INTERNAL_FRESH_TRANSITION = /(?:^|\b)(?:\/fresh\s+(?:task|scout|be-to-fe)|(?:task|scout|be-to-fe):)\b[\s\S]{0,180}\bRead task intake from \.pi\/task-inbox\//i;
|
|
58
|
+
|
|
59
|
+
function atWordBoundary(value: string, maximum: number): string {
|
|
60
|
+
if (value.length <= maximum) return value;
|
|
61
|
+
const slice = value.slice(0, maximum + 1), boundary = slice.lastIndexOf(" ");
|
|
62
|
+
return `${slice.slice(0, boundary >= Math.floor(maximum * .6) ? boundary : maximum).trim()}…`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Session titles are a local deterministic projection. Asking a model to name
|
|
66
|
+
// every chat would spend a turn and make opening the catalog non-deterministic.
|
|
67
|
+
// Runtime routing commands are control-plane text, so they must never become a
|
|
68
|
+
// title or preview merely because Pi records them as the first user entry.
|
|
69
|
+
export function projectedSessionTitle(info: Pick<PiSessionInfo, "name" | "firstMessage" | "cwd">): { title: string; preview: string } {
|
|
70
|
+
const projectLabel = display(info.cwd ? path.basename(info.cwd) : "", 120, "Unknown project");
|
|
71
|
+
const first = info.firstMessage === "(no messages)" ? "" : display(info.firstMessage, 500, "");
|
|
72
|
+
const rawName = display(info.name, 500, ""), generatedName = /^pi:/i.test(rawName);
|
|
73
|
+
const suppliedName = rawName.replace(/^pi:\s*/i, "");
|
|
74
|
+
const normalizedName = suppliedName.toLowerCase();
|
|
75
|
+
const genericName = !suppliedName || normalizedName === projectLabel.toLowerCase()
|
|
76
|
+
|| ["working", "pi agent platform", "new conversation"].includes(normalizedName);
|
|
77
|
+
const source = generatedName ? suppliedName || first : genericName ? first || suppliedName : suppliedName;
|
|
78
|
+
const internal = INTERNAL_FRESH_TRANSITION.test(source) || INTERNAL_FRESH_TRANSITION.test(first)
|
|
79
|
+
|| /\.pi\/task-inbox\//i.test(source);
|
|
80
|
+
const cleaned = display(source, 500, "")
|
|
81
|
+
.replace(/^\/(?:fresh\s+)?(?:task|scout|be-to-fe)\s+/i, "")
|
|
82
|
+
.replace(/^(?:task|scout|be-to-fe):\s*/i, "")
|
|
83
|
+
.replace(/^[-#*>\s]+/, "")
|
|
84
|
+
// Keep underscores: the shared redactor deliberately emits
|
|
85
|
+
// [REDACTED_SECRET], and title cleanup must not mutate that proof marker.
|
|
86
|
+
.replace(/[`*~]+/g, "")
|
|
87
|
+
.trim();
|
|
88
|
+
const title = internal ? "Continued task" : atWordBoundary(cleaned || "New conversation", 72);
|
|
89
|
+
const preview = INTERNAL_FRESH_TRANSITION.test(first) || /\.pi\/task-inbox\//i.test(first)
|
|
90
|
+
? "Continued in a fresh session" : atWordBoundary(first || title, 180);
|
|
91
|
+
return { title, preview };
|
|
92
|
+
}
|
|
93
|
+
|
|
36
94
|
function revision(key: Buffer, value: unknown): string {
|
|
37
95
|
return `rev_${createHmac("sha256", key).update(JSON.stringify(value)).digest("hex")}`;
|
|
38
96
|
}
|
|
@@ -40,18 +98,13 @@ function revision(key: Buffer, value: unknown): string {
|
|
|
40
98
|
function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undefined, metadataReason: string | null,
|
|
41
99
|
ownership?: SessionOwnerProjection, sessionOptions?: SessionOptionProjection): SessionRow {
|
|
42
100
|
const projectLabel = display(info.cwd ? path.basename(info.cwd) : "", 120, "Unknown project");
|
|
43
|
-
const
|
|
44
|
-
const suppliedName = display(info.name, 500, "").replace(/^pi:\s*/i, "");
|
|
45
|
-
const normalizedName = suppliedName.toLowerCase();
|
|
46
|
-
const genericName = !suppliedName || normalizedName === projectLabel.toLowerCase()
|
|
47
|
-
|| ["working", "pi agent platform", "new conversation"].includes(normalizedName);
|
|
48
|
-
const title = display(genericName && first ? first : suppliedName || first, 500, "New conversation").replace(/^pi:\s*/i, "") || "New conversation";
|
|
101
|
+
const projected = projectedSessionTitle(info);
|
|
49
102
|
return {
|
|
50
103
|
sessionRef: sessionRefForPath(key, info.path),
|
|
51
104
|
projectRef: projectRefForCwd(key, info.cwd),
|
|
52
|
-
title,
|
|
105
|
+
title: projected.title,
|
|
53
106
|
projectLabel,
|
|
54
|
-
preview:
|
|
107
|
+
preview: projected.preview,
|
|
55
108
|
createdAt: info.created.toISOString(),
|
|
56
109
|
updatedAt: info.modified.toISOString(),
|
|
57
110
|
state: metadata?.archived ? "archived" : ownership?.state ?? "offline",
|
|
@@ -87,7 +140,7 @@ export async function buildSessionCatalog(options: {
|
|
|
87
140
|
}): Promise<Catalog> {
|
|
88
141
|
const limit = Math.max(1, Math.min(200, options.limit ?? 200));
|
|
89
142
|
try {
|
|
90
|
-
const found = await options.listSessions();
|
|
143
|
+
const found = (await options.listSessions()).filter(isUserConversationSession);
|
|
91
144
|
const metadata = options.readMetadata?.() ?? { state: "ready", revision: null, sessions: new Map(), reasonCode: null };
|
|
92
145
|
const sessions = found.map((info) => {
|
|
93
146
|
const sessionRef = sessionRefForPath(options.key, info.path);
|
|
@@ -35,7 +35,10 @@ function nullableRef(value: unknown): value is string | null { return value ===
|
|
|
35
35
|
|
|
36
36
|
function validPayload(action: SessionAction, payload: Record<string, unknown>): boolean {
|
|
37
37
|
if (["session.archive", "session.unarchive", "session.acquire", "session.release"].includes(action)) return exactKeys(payload, []);
|
|
38
|
-
if (action === "session.send") return exactKeys(payload, ["delivery", "message", "messageRequestId", "expectedOperationRef"])
|
|
38
|
+
if (action === "session.send") return exactKeys(payload, ["delivery", "message", "messageRequestId", "expectedOperationRef", "attachmentRefs"])
|
|
39
|
+
&& Array.isArray(payload.attachmentRefs) && payload.attachmentRefs.length <= 4
|
|
40
|
+
&& new Set(payload.attachmentRefs as unknown[]).size === payload.attachmentRefs.length
|
|
41
|
+
&& (payload.attachmentRefs as unknown[]).every((item) => typeof item === "string" && REF.test(item))
|
|
39
42
|
&& ["new-operation", "follow-up", "steer"].includes(String(payload.delivery)) && typeof payload.message === "string"
|
|
40
43
|
&& payload.message.length >= 1 && payload.message.length <= 32_768 && !payload.message.includes("\0") && REF.test(String(payload.messageRequestId))
|
|
41
44
|
&& nullableRef(payload.expectedOperationRef) && (payload.delivery === "new-operation" ? payload.expectedOperationRef === null : payload.expectedOperationRef !== null);
|
|
@@ -52,11 +55,12 @@ function validPayload(action: SessionAction, payload: Record<string, unknown>):
|
|
|
52
55
|
if (action === "session.fork") return exactKeys(payload, ["entryRef", "title"]) && nullableRef(payload.entryRef)
|
|
53
56
|
&& (payload.title === null || typeof payload.title === "string" && payload.title.length >= 1 && payload.title.length <= 500
|
|
54
57
|
&& !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(payload.title));
|
|
55
|
-
|
|
58
|
+
const createKeys = ["projectRef", "placeRef", "modelRef", "thinkingLevel", "message", "messageRequestId"];
|
|
59
|
+
return (exactKeys(payload, createKeys) || exactKeys(payload, [...createKeys, "deferInitialMessage"]))
|
|
56
60
|
&& REF.test(String(payload.projectRef)) && REF.test(String(payload.placeRef)) && nullableRef(payload.modelRef)
|
|
57
61
|
&& ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(payload.thinkingLevel))
|
|
58
62
|
&& typeof payload.message === "string" && payload.message.length >= 1 && payload.message.length <= 32_768 && !payload.message.includes("\0")
|
|
59
|
-
&& REF.test(String(payload.messageRequestId));
|
|
63
|
+
&& REF.test(String(payload.messageRequestId)) && (payload.deferInitialMessage === undefined || typeof payload.deferInitialMessage === "boolean");
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
function parseCommand(value: unknown): SessionCommand | null {
|
|
@@ -86,10 +90,15 @@ export class SessionCommandController {
|
|
|
86
90
|
readonly #metadata: SessionMetadataStore | null;
|
|
87
91
|
readonly #now: () => Date;
|
|
88
92
|
readonly #chains = new Map<string, Promise<unknown>>();
|
|
93
|
+
readonly #prepareAttachments: ((sessionRef: string, refs: string[], messageRequestId: string, text: string) => Promise<{
|
|
94
|
+
text: string; images: unknown[]; commit(): void; release(): void }>) | undefined;
|
|
89
95
|
|
|
90
96
|
constructor(options: { catalog(): Promise<Catalog>; runtimes: SessionRuntimeSupervisor; store: SessionCommandStore; events: GatewayEventStore;
|
|
91
97
|
metadata?: SessionMetadataStore;
|
|
98
|
+
prepareAttachments?(sessionRef: string, refs: string[], messageRequestId: string, text: string): Promise<{
|
|
99
|
+
text: string; images: unknown[]; commit(): void; release(): void }>;
|
|
92
100
|
now?: () => Date }) {
|
|
101
|
+
this.#prepareAttachments = options.prepareAttachments;
|
|
93
102
|
this.#catalog = options.catalog; this.#runtimes = options.runtimes; this.#store = options.store; this.#events = options.events;
|
|
94
103
|
this.#metadata = options.metadata ?? null;
|
|
95
104
|
this.#now = options.now ?? (() => new Date());
|
|
@@ -129,8 +138,10 @@ export class SessionCommandController {
|
|
|
129
138
|
try { this.#store.admit(command, this.#now()); }
|
|
130
139
|
catch { return this.#rejected(command, current, row, "unavailable", "session-command-admission-failed"); }
|
|
131
140
|
let targetSessionRef = command.sessionRef;
|
|
141
|
+
let attachmentReservation: { commit(): void; release(): void } | null = null;
|
|
142
|
+
let sendAttempted = false;
|
|
132
143
|
try {
|
|
133
|
-
let resultCode: "acquired" | "released" | "started" | "queued" | "steered" | "aborted" | "model-changed" | "thinking-changed" | "permission-changed"
|
|
144
|
+
let resultCode: "created" | "acquired" | "released" | "started" | "queued" | "steered" | "aborted" | "model-changed" | "thinking-changed" | "permission-changed"
|
|
134
145
|
| "renamed" | "pinned" | "unpinned" | "archived" | "unarchived" | "forked" | "no-change",
|
|
135
146
|
operationRef: string | null = null;
|
|
136
147
|
if (command.action === "session.create") {
|
|
@@ -138,18 +149,32 @@ export class SessionCommandController {
|
|
|
138
149
|
command.payload.modelRef === null ? null : String(command.payload.modelRef), String(command.payload.thinkingLevel));
|
|
139
150
|
const created = await this.#readyCatalog(), createdRow = created.sessions.find((item) => item.sessionRef === targetSessionRef);
|
|
140
151
|
if (!createdRow) throw new Error("session-catalog-refresh-failed");
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
if (command.payload.deferInitialMessage === true) resultCode = "created";
|
|
153
|
+
else {
|
|
154
|
+
const sent = await this.#runtimes.send(targetSessionRef, { delivery: "new-operation", message: String(command.payload.message),
|
|
155
|
+
expectedOperationRef: null }, createdRow.sessionRevision);
|
|
156
|
+
resultCode = sent.resultCode === "started" ? "started" : sent.resultCode; operationRef = sent.operationRef;
|
|
157
|
+
}
|
|
144
158
|
} else if (command.action === "session.acquire") { await this.#runtimes.acquire(command.sessionRef!); resultCode = "acquired"; }
|
|
145
159
|
else if (command.action === "session.release") { await this.#runtimes.release(command.sessionRef!); resultCode = "released"; }
|
|
146
160
|
else if (command.action === "session.send") {
|
|
147
161
|
await this.#runtimes.acquire(command.sessionRef!);
|
|
148
162
|
const acquired = await this.#readyCatalog(), acquiredRow = acquired.sessions.find((item) => item.sessionRef === command.sessionRef);
|
|
149
163
|
if (!acquiredRow) throw new Error("session-catalog-refresh-failed");
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
const payload = command.payload as { delivery: "new-operation" | "follow-up" | "steer"; message: string;
|
|
165
|
+
expectedOperationRef: string | null; messageRequestId: string; attachmentRefs: string[] };
|
|
166
|
+
// Staged refs are claimed once, here, before the runtime is asked to
|
|
167
|
+
// dispatch. Documents were turned into text at staging so they join the
|
|
168
|
+
// message; images travel on the host's own channel.
|
|
169
|
+
const prepared = this.#prepareAttachments
|
|
170
|
+
? await this.#prepareAttachments(command.sessionRef!, payload.attachmentRefs, payload.messageRequestId, payload.message)
|
|
171
|
+
: { text: payload.message, images: [] as unknown[], commit() {}, release() {} };
|
|
172
|
+
attachmentReservation = prepared;
|
|
173
|
+
if (!this.#prepareAttachments && payload.attachmentRefs.length > 0) throw new Error("session-attachment-unavailable");
|
|
174
|
+
sendAttempted = true;
|
|
175
|
+
const sent = await this.#runtimes.send(command.sessionRef!, { ...payload, message: prepared.text, images: prepared.images },
|
|
176
|
+
acquiredRow.sessionRevision);
|
|
177
|
+
prepared.commit();
|
|
153
178
|
resultCode = sent.resultCode; operationRef = sent.operationRef;
|
|
154
179
|
} else if (command.action === "session.abort") {
|
|
155
180
|
operationRef = String(command.payload.operationRef);
|
|
@@ -195,10 +220,18 @@ export class SessionCommandController {
|
|
|
195
220
|
return receipt;
|
|
196
221
|
} catch (cause) {
|
|
197
222
|
const after = await this.#readyCatalog(), afterRow = after.sessions.find((item) => item.sessionRef === targetSessionRef) ?? row;
|
|
198
|
-
const
|
|
199
|
-
|
|
223
|
+
const message = cause instanceof Error ? cause.message : "session-command-failed";
|
|
224
|
+
const noDispatchEffect = command.action === "session.send" && (!sendAttempted
|
|
225
|
+
|| /^(session-runtime-unavailable|session-operation-conflict|session-owner-conflict|session-runtime-busy)$/.test(message));
|
|
226
|
+
if (attachmentReservation) {
|
|
227
|
+
try { if (noDispatchEffect) attachmentReservation.release(); else attachmentReservation.commit(); }
|
|
228
|
+
catch { /* an attempted dispatch with an unprovable reservation remains uncertain */ }
|
|
229
|
+
}
|
|
230
|
+
const code = /recovery/.test(message) ? "recovery-required"
|
|
231
|
+
: /(owner-conflict|operation-conflict|runtime-busy)/.test(message) ? "owner-conflict"
|
|
232
|
+
: noDispatchEffect || command.action === "session.send" && !sendAttempted ? "unavailable" : "effect-unknown";
|
|
200
233
|
const receipt = code === "effect-unknown" ? this.#uncertain(command, after, afterRow, "session-command-effect-unknown")
|
|
201
|
-
: this.#rejected(command, after, afterRow, code,
|
|
234
|
+
: this.#rejected(command, after, afterRow, code, message);
|
|
202
235
|
try { this.#store.settle(command, receipt, this.#now()); } catch { /* intent remains uncertain */ }
|
|
203
236
|
return receipt;
|
|
204
237
|
}
|
|
@@ -219,7 +252,7 @@ export class SessionCommandController {
|
|
|
219
252
|
sessionRevisionAfter: row?.sessionRevision ?? null, deduplicated: false };
|
|
220
253
|
}
|
|
221
254
|
#settled(command: SessionCommand, catalog: Catalog, row: SessionRow,
|
|
222
|
-
resultCode: "acquired" | "released" | "started" | "queued" | "steered" | "aborted" | "model-changed" | "thinking-changed" | "permission-changed"
|
|
255
|
+
resultCode: "created" | "acquired" | "released" | "started" | "queued" | "steered" | "aborted" | "model-changed" | "thinking-changed" | "permission-changed"
|
|
223
256
|
| "renamed" | "pinned" | "unpinned" | "archived" | "unarchived" | "forked" | "no-change",
|
|
224
257
|
operationRef: string | null): Receipt {
|
|
225
258
|
return { ...this.#base(command, catalog, row), operationRef, phase: "settled", resultCode, settledAt: this.#now().toISOString(),
|
|
@@ -3,25 +3,30 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { readContextTelemetry } from "../../piagent-core/extensions/context-engine.js";
|
|
5
5
|
import { activeSessionTask } from "../../piagent-core/extensions/task-state.js";
|
|
6
|
-
import { runtimeConnectionDefinitions, runtimeProtectedPaths } from "../../piagent-core/runtime/inspection/project-runtime-inspection.ts";
|
|
6
|
+
import { runtimeConnectionDefinitions, runtimeDocumentReadRoots, runtimeProtectedPaths } from "../../piagent-core/runtime/inspection/project-runtime-inspection.ts";
|
|
7
7
|
import { setRuntimeMcpEnabled } from "../../piagent-core/runtime/inspection/mcp-control.ts";
|
|
8
8
|
import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
|
|
9
9
|
import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
|
|
10
10
|
import { piApprovalBroker } from "../../piagent-core/runtime/inspection/approval-broker.ts";
|
|
11
|
+
import { attachmentCapability } from "../../piagent-core/runtime/input/attachment-store.ts";
|
|
11
12
|
import { CoreInspectionProvider } from "../server/core-inspection-provider.ts";
|
|
12
13
|
import { ReadModelNotFound, type WebUiReadModelProvider } from "../server/read-model-provider.ts";
|
|
13
|
-
import { projectRefForCwd, sessionRefForPath, type PiSessionInfo } from "./session-catalog.ts";
|
|
14
|
+
import { isUserConversationSession, projectRefForCwd, sessionRefForPath, type PiSessionInfo } from "./session-catalog.ts";
|
|
14
15
|
import { nativeProjectPickerAvailable } from "./native-project-picker.ts";
|
|
15
16
|
import type { McpAuthBroker } from "./mcp-auth-broker.ts";
|
|
17
|
+
import { inspectWebSearchCapability } from "./ai-capability-inspection.ts";
|
|
16
18
|
|
|
17
19
|
type PiHost = {
|
|
18
20
|
SessionManager: {
|
|
19
21
|
listAll(): Promise<PiSessionInfo[]>;
|
|
20
22
|
open(file: string): {
|
|
21
23
|
getBranch(): unknown[];
|
|
22
|
-
buildSessionContext(): { model: { provider: string; modelId: string } | null; thinkingLevel: string };
|
|
24
|
+
buildSessionContext(): { model: { provider: string; modelId: string } | null; thinkingLevel: string; messages: unknown[] };
|
|
23
25
|
};
|
|
24
26
|
};
|
|
27
|
+
calculateContextTokens(usage: unknown): number;
|
|
28
|
+
estimateTokens(message: unknown): number;
|
|
29
|
+
getLatestCompactionEntry(entries: unknown[]): unknown | null;
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
type ModelRuntime = {
|
|
@@ -59,22 +64,59 @@ function safeModelId(value: unknown): string | null {
|
|
|
59
64
|
return redactSensitiveText(value).redacted ? null : value;
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
function persistedContextUsage(host: PiHost, entries: unknown[], messages: unknown[], model: Record<string, unknown> | undefined) {
|
|
68
|
+
const contextWindow = Number(model?.contextWindow);
|
|
69
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) return undefined;
|
|
70
|
+
const latestCompaction = host.getLatestCompactionEntry(entries);
|
|
71
|
+
if (latestCompaction) {
|
|
72
|
+
const boundary = entries.lastIndexOf(latestCompaction);
|
|
73
|
+
let hasPostCompactionUsage = false;
|
|
74
|
+
for (let index = entries.length - 1; index > boundary; index -= 1) {
|
|
75
|
+
const entry = entries[index] as { type?: unknown; message?: { role?: unknown; stopReason?: unknown; usage?: unknown } } | undefined;
|
|
76
|
+
const message = entry?.type === "message" ? entry.message : undefined;
|
|
77
|
+
if (message?.role === "assistant" && message.usage && message.stopReason !== "aborted" && message.stopReason !== "error"
|
|
78
|
+
&& host.calculateContextTokens(message.usage) > 0) {
|
|
79
|
+
hasPostCompactionUsage = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!hasPostCompactionUsage) return { tokens: null, contextWindow, percent: null };
|
|
84
|
+
}
|
|
85
|
+
let lastUsageIndex = -1, tokens = 0;
|
|
86
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
87
|
+
const message = messages[index] as { role?: unknown; stopReason?: unknown; usage?: unknown } | undefined;
|
|
88
|
+
if (message?.role !== "assistant" || !message.usage || message.stopReason === "aborted" || message.stopReason === "error") continue;
|
|
89
|
+
const measured = host.calculateContextTokens(message.usage);
|
|
90
|
+
if (measured > 0) { tokens = measured; lastUsageIndex = index; break; }
|
|
91
|
+
}
|
|
92
|
+
for (let index = lastUsageIndex + 1; index < messages.length; index += 1) tokens += host.estimateTokens(messages[index]);
|
|
93
|
+
if (!Number.isFinite(tokens) || tokens < 0) return undefined;
|
|
94
|
+
return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
|
|
95
|
+
}
|
|
96
|
+
|
|
62
97
|
export class SessionInspectionRegistry {
|
|
63
98
|
readonly #gatewayInstanceRef: string;
|
|
64
99
|
readonly #host: PiHost;
|
|
100
|
+
readonly #listSessions: () => Promise<PiSessionInfo[]>;
|
|
101
|
+
readonly #openLiveSession: ((sessionRef: string) => ReturnType<PiHost["SessionManager"]["open"]> | null) | null;
|
|
65
102
|
readonly #key: Buffer;
|
|
66
103
|
readonly #packageRoot: string;
|
|
104
|
+
readonly #agentDir?: string;
|
|
67
105
|
readonly #models?: ModelRuntime;
|
|
68
106
|
readonly #projects?: ProjectSource;
|
|
69
107
|
readonly #mcpAuth?: McpAuthBroker;
|
|
70
108
|
readonly #providers = new Map<string, { modifiedAt: string; provider: CoreInspectionProvider }>();
|
|
71
109
|
|
|
72
|
-
constructor(options: { gatewayInstanceRef: string; host: PiHost; key: Buffer; packageRoot: string; models?: ModelRuntime; projects?: ProjectSource;
|
|
73
|
-
mcpAuth?: McpAuthBroker
|
|
110
|
+
constructor(options: { gatewayInstanceRef: string; host: PiHost; key: Buffer; packageRoot: string; agentDir?: string; models?: ModelRuntime; projects?: ProjectSource;
|
|
111
|
+
mcpAuth?: McpAuthBroker; listSessions?: () => Promise<PiSessionInfo[]>;
|
|
112
|
+
openLiveSession?: (sessionRef: string) => ReturnType<PiHost["SessionManager"]["open"]> | null }) {
|
|
74
113
|
this.#gatewayInstanceRef = options.gatewayInstanceRef;
|
|
75
114
|
this.#host = options.host;
|
|
115
|
+
this.#listSessions = options.listSessions ?? (() => options.host.SessionManager.listAll());
|
|
116
|
+
this.#openLiveSession = options.openLiveSession ?? null;
|
|
76
117
|
this.#key = options.key;
|
|
77
118
|
this.#packageRoot = options.packageRoot;
|
|
119
|
+
this.#agentDir = options.agentDir;
|
|
78
120
|
this.#models = options.models;
|
|
79
121
|
this.#projects = options.projects;
|
|
80
122
|
this.#mcpAuth = options.mcpAuth;
|
|
@@ -85,7 +127,7 @@ export class SessionInspectionRegistry {
|
|
|
85
127
|
}
|
|
86
128
|
|
|
87
129
|
async #sessionInfo(sessionRef: string): Promise<PiSessionInfo> {
|
|
88
|
-
const sessions = await this.#
|
|
130
|
+
const sessions = (await this.#listSessions()).filter(isUserConversationSession);
|
|
89
131
|
const found = sessions.filter((candidate) => sessionRefForPath(this.#key, candidate.path) === sessionRef);
|
|
90
132
|
if (found.length !== 1) throw new ReadModelNotFound();
|
|
91
133
|
return found[0]!;
|
|
@@ -135,7 +177,7 @@ export class SessionInspectionRegistry {
|
|
|
135
177
|
}
|
|
136
178
|
|
|
137
179
|
async creationOptions(): Promise<unknown> {
|
|
138
|
-
const sessions = await this.#
|
|
180
|
+
const sessions = (await this.#listSessions()).filter(isUserConversationSession);
|
|
139
181
|
const projects = new Map<string, { projectRef: string; placeRef: string; label: string }>();
|
|
140
182
|
for (const info of sessions) {
|
|
141
183
|
if (!info.cwd) continue;
|
|
@@ -146,7 +188,8 @@ export class SessionInspectionRegistry {
|
|
|
146
188
|
for (const project of safeRead(() => this.#projects?.list() ?? [], [])) {
|
|
147
189
|
if (!projects.has(project.projectRef)) projects.set(project.projectRef, project);
|
|
148
190
|
}
|
|
149
|
-
const
|
|
191
|
+
const availableModels = (this.#models?.getAvailableSnapshot() ?? []).slice(0, 300);
|
|
192
|
+
const models = availableModels.flatMap((value) => {
|
|
150
193
|
const provider = safeModelId(value.provider);
|
|
151
194
|
const modelId = safeModelId(value.id);
|
|
152
195
|
if (!provider || !modelId || typeof value.reasoning !== "boolean") return [];
|
|
@@ -154,11 +197,12 @@ export class SessionInspectionRegistry {
|
|
|
154
197
|
? value.thinkingLevelMap as Record<string, unknown> : {};
|
|
155
198
|
const thinkingLevels = value.reasoning ? ["off", "minimal", "low", "medium", "high", "xhigh", "max"]
|
|
156
199
|
.filter((level) => mapping[level] !== null && !(["xhigh", "max"].includes(level) && mapping[level] === undefined)) : ["off"];
|
|
200
|
+
const inputs = Array.isArray(value.input) ? value.input.filter((item): item is string => typeof item === "string") : null;
|
|
157
201
|
return [{ modelRef: webUiModelRef(provider, modelId), provider, modelId, displayName: safeName(value.name ?? modelId),
|
|
158
|
-
reasoning: value.reasoning, thinkingLevels }];
|
|
202
|
+
reasoning: value.reasoning, imageInput: inputs ? inputs.includes("image") : null, thinkingLevels }];
|
|
159
203
|
});
|
|
160
204
|
return { schemaVersion: 1, version: "piagent-session-creation-options-v1", generatedAt: new Date().toISOString(),
|
|
161
|
-
projects: [...projects.values()].slice(0, 200), models,
|
|
205
|
+
projects: [...projects.values()].slice(0, 200), models, webSearch: inspectWebSearchCapability({ agentDir: this.#agentDir, models: availableModels }),
|
|
162
206
|
projectImport: nativeProjectPickerAvailable() ? { status: "available", reasonCode: null }
|
|
163
207
|
: { status: "unavailable", reasonCode: "native-project-picker-unavailable" },
|
|
164
208
|
reasonCode: projects.size ? null : "no-known-project" };
|
|
@@ -171,11 +215,12 @@ export class SessionInspectionRegistry {
|
|
|
171
215
|
if (cached?.modifiedAt === modifiedAt) return cached.provider;
|
|
172
216
|
|
|
173
217
|
let manager: ReturnType<PiHost["SessionManager"]["open"]>;
|
|
174
|
-
try { manager = this.#host.SessionManager.open(info.path); }
|
|
218
|
+
try { manager = this.#openLiveSession?.(sessionRef) ?? this.#host.SessionManager.open(info.path); }
|
|
175
219
|
catch { throw new ReadModelNotFound(); }
|
|
176
|
-
const context = safeRead(() => manager.buildSessionContext(), { model: null, thinkingLevel: "unknown" });
|
|
220
|
+
const context = safeRead(() => manager.buildSessionContext(), { model: null, thinkingLevel: "unknown", messages: [] });
|
|
177
221
|
const model = context.model ? this.#models?.getModel(context.model.provider, context.model.modelId) : undefined;
|
|
178
222
|
const entries = safeRead(() => manager.getBranch(), []);
|
|
223
|
+
const contextUsage = safeRead(() => persistedContextUsage(this.#host, entries, context.messages, model), undefined);
|
|
179
224
|
const eventStore = {
|
|
180
225
|
retention: () => ({ eventRetentionCount: 0, eventRetentionSeconds: 0 }),
|
|
181
226
|
currentCursor: () => null,
|
|
@@ -191,6 +236,16 @@ export class SessionInspectionRegistry {
|
|
|
191
236
|
activityEvents: () => safeRead(() => readContextTelemetry(info.cwd, { limit: 5_000 }), []),
|
|
192
237
|
sessionEntries: () => entries,
|
|
193
238
|
protectedPaths: () => runtimeProtectedPaths(this.#packageRoot, info.cwd),
|
|
239
|
+
// The document workspace lists the project plus whatever the operator
|
|
240
|
+
// granted in the profile. Without this it silently shows the project only,
|
|
241
|
+
// which reads as "the grant does not work" rather than "it was not asked for".
|
|
242
|
+
documentReadRoots: () => safeRead(() => runtimeDocumentReadRoots(this.#packageRoot, info.cwd), []),
|
|
243
|
+
// Derived from the model and the host, with no store instance involved:
|
|
244
|
+
// the store for this session is built from the very snapshot this call
|
|
245
|
+
// helps produce, so asking the store would be circular.
|
|
246
|
+
attachmentCapability: () => attachmentCapability({
|
|
247
|
+
images: Array.isArray(model?.input) && model.input.includes("image"), now: Date.now() }),
|
|
248
|
+
contextUsage: () => contextUsage,
|
|
194
249
|
model: () => model,
|
|
195
250
|
thinkingLevel: () => context.thinkingLevel,
|
|
196
251
|
approvalProjection: () => piApprovalBroker.projection(info.cwd, info.id),
|
|
@@ -7,11 +7,12 @@ import { activeSessionTask } from "../../piagent-core/extensions/task-state.js";
|
|
|
7
7
|
import { inspectTaskControlState } from "../../piagent-core/runtime/inspection/task-control-journal.ts";
|
|
8
8
|
import { piApprovalBroker, type ApprovalAuthority, type ApprovalBrokerEvent } from "../../piagent-core/runtime/inspection/approval-broker.ts";
|
|
9
9
|
import type { SessionOwnerProjection, PiSessionInfo } from "./session-catalog.ts";
|
|
10
|
-
import { projectRefForCwd, sessionRefForPath } from "./session-catalog.ts";
|
|
10
|
+
import { isUserConversationSession, projectRefForCwd, sessionRefForPath } from "./session-catalog.ts";
|
|
11
11
|
import { SessionLeaseStore, type SessionLeaseSnapshot } from "./session-lease-store.ts";
|
|
12
12
|
import { GatewayEventStore } from "./gateway-events.ts";
|
|
13
13
|
import { GatewaySessionStream } from "./gateway-session-stream.ts";
|
|
14
14
|
import { GATEWAY_RUNTIME_UI_MARKER } from "../ownership/gateway-runtime-context.ts";
|
|
15
|
+
import { preferAuthoritativePiagentGuard } from "./extension-authority.ts";
|
|
15
16
|
|
|
16
17
|
const MAX_WARM_RUNTIMES = 10;
|
|
17
18
|
|
|
@@ -19,7 +20,7 @@ type RuntimeHandle = { dispose(): Promise<void>; session?: any };
|
|
|
19
20
|
type RuntimeFactory = (info: PiSessionInfo, runtimeInstanceRef: string, sessionManager?: any) => Promise<RuntimeHandle>;
|
|
20
21
|
type ActiveRuntime = { runtime: RuntimeHandle; lease: SessionLeaseSnapshot; info: PiSessionInfo; operationRef: string | null;
|
|
21
22
|
unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
|
|
22
|
-
unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null };
|
|
23
|
+
unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null; sessionManager: any | null };
|
|
23
24
|
type Projection = { sessionRevision: string; liveState: "offline" | "idle" | "running" | "paused" | "waiting-approval" | "uncertain" };
|
|
24
25
|
|
|
25
26
|
function rpcUiContext(): object {
|
|
@@ -55,7 +56,10 @@ function productionRuntimeFactory(options: { host: any; agentDir: string; packag
|
|
|
55
56
|
const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }: any) => {
|
|
56
57
|
const services = await options.host.createAgentSessionServices({
|
|
57
58
|
cwd, agentDir, modelRuntime: options.modelRuntime,
|
|
58
|
-
resourceLoaderOptions: {
|
|
59
|
+
resourceLoaderOptions: {
|
|
60
|
+
additionalExtensionPaths: [guard],
|
|
61
|
+
extensionsOverride: preferAuthoritativePiagentGuard(guard)
|
|
62
|
+
}
|
|
59
63
|
});
|
|
60
64
|
const extensionErrors = services.resourceLoader.getExtensions().errors;
|
|
61
65
|
if (extensionErrors.length) throw new Error("session-runtime-extension-load-failed");
|
|
@@ -125,7 +129,7 @@ export class SessionRuntimeSupervisor {
|
|
|
125
129
|
setProjectionReader(reader: (sessionRef: string) => Promise<Projection>): void { this.#readProjection = reader; }
|
|
126
130
|
|
|
127
131
|
async listSessions(): Promise<PiSessionInfo[]> {
|
|
128
|
-
const persisted = await this.#listSessions();
|
|
132
|
+
const persisted = (await this.#listSessions()).filter(isUserConversationSession);
|
|
129
133
|
const paths = new Set(persisted.map((info) => info.path));
|
|
130
134
|
for (const [sessionRef, info] of this.#created) {
|
|
131
135
|
if (paths.has(info.path)) this.#created.delete(sessionRef);
|
|
@@ -136,7 +140,7 @@ export class SessionRuntimeSupervisor {
|
|
|
136
140
|
async create(projectRef: string, placeRef: string, modelRef: string | null, thinkingLevel: string): Promise<string> {
|
|
137
141
|
if (this.#closed || !this.#host) throw new Error("session-create-unavailable");
|
|
138
142
|
if (placeRef !== projectRef) throw new Error("session-place-mismatch");
|
|
139
|
-
const sessions = await this.#listSessions();
|
|
143
|
+
const sessions = (await this.#listSessions()).filter(isUserConversationSession);
|
|
140
144
|
const imported = this.#resolveProject?.(projectRef) ?? null;
|
|
141
145
|
const candidates = [...new Set([...sessions.filter((info) => projectRefForCwd(this.#key, info.cwd) === projectRef).map((info) => info.cwd),
|
|
142
146
|
...(imported ? [imported] : [])])];
|
|
@@ -162,6 +166,8 @@ export class SessionRuntimeSupervisor {
|
|
|
162
166
|
return sessionRef;
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
liveSessionManager(sessionRef: string): any | null { return this.#active.get(sessionRef)?.sessionManager ?? null; }
|
|
170
|
+
|
|
165
171
|
async acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
|
|
166
172
|
if (this.#closed) throw new Error("session-runtime-supervisor-closed");
|
|
167
173
|
const active = this.#active.get(sessionRef);
|
|
@@ -175,7 +181,8 @@ export class SessionRuntimeSupervisor {
|
|
|
175
181
|
|
|
176
182
|
async #acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
|
|
177
183
|
if (this.#active.size + this.#opening.size > MAX_WARM_RUNTIMES) throw new Error("session-runtime-capacity");
|
|
178
|
-
const found = (await this.#listSessions()).filter(
|
|
184
|
+
const found = (await this.#listSessions()).filter(isUserConversationSession)
|
|
185
|
+
.filter((info) => sessionRefForPath(this.#key, info.path) === sessionRef);
|
|
179
186
|
if (found.length !== 1) throw new Error(found.length ? "session-ref-ambiguous" : "session-not-found");
|
|
180
187
|
return await this.#activate(sessionRef, found[0]!);
|
|
181
188
|
}
|
|
@@ -197,7 +204,8 @@ export class SessionRuntimeSupervisor {
|
|
|
197
204
|
throw new Error("session-owner-continuity-lost");
|
|
198
205
|
}
|
|
199
206
|
const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, unsubscribe: null, completion: null,
|
|
200
|
-
settling: false, approvalWaiting: false, unbindApproval: null, unsubscribeApproval: null
|
|
207
|
+
settling: false, approvalWaiting: false, unbindApproval: null, unsubscribeApproval: null,
|
|
208
|
+
sessionManager: sessionManager ?? runtime.session?.sessionManager ?? null };
|
|
201
209
|
this.#active.set(sessionRef, active);
|
|
202
210
|
this.#bindApproval(sessionRef, active);
|
|
203
211
|
return current;
|
|
@@ -267,15 +275,20 @@ export class SessionRuntimeSupervisor {
|
|
|
267
275
|
};
|
|
268
276
|
}
|
|
269
277
|
|
|
278
|
+
// `message` arrives already carrying any attached document text, and `images`
|
|
279
|
+
// already split out: the command controller claims staged refs before it calls
|
|
280
|
+
// here, so this layer only forwards what the host prompt takes.
|
|
270
281
|
async send(sessionRef: string, payload: { delivery: "new-operation" | "follow-up" | "steer"; message: string;
|
|
271
|
-
expectedOperationRef: string | null }, sessionRevision: string):
|
|
282
|
+
expectedOperationRef: string | null; images?: unknown[] }, sessionRevision: string):
|
|
283
|
+
Promise<{ resultCode: "started" | "queued" | "steered"; operationRef: string }> {
|
|
272
284
|
await this.acquire(sessionRef);
|
|
273
285
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
274
286
|
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
275
287
|
if (payload.delivery !== "new-operation") {
|
|
276
288
|
if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || !session.isStreaming) throw new Error("session-operation-conflict");
|
|
277
|
-
|
|
278
|
-
await session.
|
|
289
|
+
const images = payload.images?.length ? payload.images : undefined;
|
|
290
|
+
if (payload.delivery === "follow-up") { await session.followUp(payload.message, images); return { resultCode: "queued", operationRef: active.operationRef }; }
|
|
291
|
+
await session.steer(payload.message, images); return { resultCode: "steered", operationRef: active.operationRef };
|
|
279
292
|
}
|
|
280
293
|
if (payload.expectedOperationRef !== null || active.operationRef || active.settling || !session.isIdle) throw new Error("session-operation-conflict");
|
|
281
294
|
const operationRef = `operation_${randomBytes(24).toString("base64url")}`;
|
|
@@ -288,7 +301,7 @@ export class SessionRuntimeSupervisor {
|
|
|
288
301
|
created.firstMessage = payload.message; created.allMessagesText = payload.message; created.messageCount = 1; created.modified = new Date();
|
|
289
302
|
}
|
|
290
303
|
let prompt: Promise<void>;
|
|
291
|
-
try { prompt = session.prompt(payload.message); }
|
|
304
|
+
try { prompt = session.prompt(payload.message, payload.images?.length ? { images: payload.images } : undefined); }
|
|
292
305
|
catch (error) { await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
|
|
293
306
|
active.completion = prompt.then(() => this.#finishOperation(sessionRef, operationRef, stream),
|
|
294
307
|
() => this.#finishOperation(sessionRef, operationRef, stream));
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"@mui/icons-material": "^9.3.1",
|
|
18
18
|
"@mui/material": "^9.3.1",
|
|
19
19
|
"react": "19.2.8",
|
|
20
|
-
"react-dom": "19.2.8"
|
|
20
|
+
"react-dom": "19.2.8",
|
|
21
|
+
"react-markdown": "^10.1.0",
|
|
22
|
+
"remark-gfm": "^4.0.1"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
23
25
|
"@types/react": "19.2.18",
|