@pi-chamber/web 0.2.2 → 0.3.0
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/bin/cli.test.js +6 -8
- package/bin/lib/cli-tunnel-capabilities.js +0 -2
- package/dist/assets/{CommitSection-DPjTv8fA.js → CommitSection-BpbMC0et.js} +1 -1
- package/dist/assets/{DiagramEditor-ChOor9l9.js → DiagramEditor-dFTMKHuO.js} +1 -1
- package/dist/assets/{DiagramView-3hAT86v_.js → DiagramView-DVw_8Px1.js} +1 -1
- package/dist/assets/{DiffView-D1dyOiw2.js → DiffView-CkoY1oAM.js} +1 -1
- package/dist/assets/{FilesView-BCzghrc7.js → FilesView-C0hyg2pa.js} +6 -6
- package/dist/assets/{GitView-CYF1E1p4.js → GitView-D36hpDsb.js} +4 -4
- package/dist/assets/{JsonTreeView-CrJO6LNu.js → JsonTreeView-BF8S-bPN.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-BmKjrkGD.js → MarkdownRendererImpl-BVNChdig.js} +1 -1
- package/dist/assets/{PatchDiff-DgvPLx6x.js → PatchDiff-B--OaNB1.js} +1 -1
- package/dist/assets/{PierreDiffViewer-InnT0JVA.js → PierreDiffViewer-0Qkp9ICt.js} +1 -1
- package/dist/assets/{SessionSwitcherDropdown-C0OdvirK.js → SessionSwitcherDropdown-cFWgy1td.js} +101 -102
- package/dist/assets/SettingsView-D3ZZyjU3.js +4 -0
- package/dist/assets/SettingsWindow-BuJCvYoR.js +1 -0
- package/dist/assets/{ToolOutputDialog-gzwcb8nI.js → ToolOutputDialog-Bi9BoEe7.js} +4 -4
- package/dist/assets/{ToolPartDiffPreview-C45V5bhy.js → ToolPartDiffPreview-DGX7N4zr.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-uKfnkQWb.js → VirtualizedFileDiff-CxraYoDZ.js} +1 -1
- package/dist/assets/{collapsible-Bq8sDiKS.js → collapsible-d4-HChej.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-BPgZ_RSe.js → computeVirtualFileMetrics-BuD14qFL.js} +2 -2
- package/dist/assets/{contextFileOpenGuard-CydO7PkI.js → contextFileOpenGuard-DirV-hCs.js} +1 -1
- package/dist/assets/{index-BSuNNtBh.js → index-BWmmf5yJ.js} +1 -1
- package/dist/assets/{index-CH7o4-_q.js → index-DEYblXCW.js} +7 -7
- package/dist/assets/index-DLKBsLKY.css +1 -0
- package/dist/assets/{isDiffPlainText-f9b4cKNo.js → isDiffPlainText-LoMmNzmX.js} +1 -1
- package/dist/assets/{isFilePlainText-BjESb-gU.js → isFilePlainText-3Hj9i02t.js} +1 -1
- package/dist/assets/{main-B508FBXr.js → main-BsszlAdQ.js} +2 -2
- package/dist/assets/main-CUvZIN16.js +133 -0
- package/dist/assets/{markdownCore-BFmf8a-O.js → markdownCore-sGxk8jrc.js} +2 -2
- package/dist/assets/{miniChat-BTlz8IPL.js → miniChat-C6SDQL4G.js} +2 -2
- package/dist/assets/{mobile-DBkrRt80.js → mobile-DBW3XDmZ.js} +2 -2
- package/dist/assets/renderElectronMiniChatApp-B1_fTdN-.js +1 -0
- package/dist/assets/renderMobileApp-CFxZc_1a.js +14 -0
- package/dist/assets/{runtimeSurface-Bec4O8FD.js → runtimeSurface-9aJdtQbh.js} +1 -1
- package/dist/assets/{useInlineCommentController-CHmIm0Rr.js → useInlineCommentController-Cl--s6vE.js} +2 -2
- package/dist/assets/usePanelSlide-D5ffPTGM.js +56 -0
- package/dist/assets/{useWindowTitle-CE5N_NeU.js → useWindowTitle-C-AZD713.js} +3 -3
- package/dist/assets/{vendor-base-ui-DwfTg5wD.js → vendor-base-ui-DL8r2fgQ.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-q1yHU1R9.js → vendor-codemirror-language-data-HC9JT4I3.js} +2 -2
- package/dist/assets/{web-components-CLirSfwt.js → web-components-skJ_VpnT.js} +4 -4
- package/dist/index.html +4 -4
- package/dist/mini-chat.html +3 -3
- package/dist/mobile.html +3 -3
- package/package.json +1 -1
- package/server/lib/package-manager.js +0 -34
- package/server/lib/pi/routes.js +32 -2
- package/server/lib/pi/session-daemon/DOCUMENTATION.md +1 -1
- package/server/lib/pi/session-daemon/runtime-registry.js +3 -0
- package/server/lib/pi/session-daemon/session-daemon.js +150 -10
- package/server/lib/pi/session-daemon/session-daemon.test.js +22 -0
- package/server/lib/tunnels/DOCUMENTATION.md +6 -7
- package/server/lib/tunnels/executable-search.test.js +4 -4
- package/server/lib/tunnels/install-help.js +0 -10
- package/server/lib/tunnels/install-help.test.js +0 -9
- package/server/lib/tunnels/types.js +0 -1
- package/dist/assets/SettingsView-JAfC_cBL.js +0 -3
- package/dist/assets/SettingsWindow-BBEsjcJE.js +0 -1
- package/dist/assets/index-o4Eea60G.css +0 -1
- package/dist/assets/main-nG4uFyAy.js +0 -133
- package/dist/assets/renderElectronMiniChatApp-DW3fSR5w.js +0 -1
- package/dist/assets/renderMobileApp-9qkroKty.js +0 -14
- package/dist/assets/usePanelSlide-DCX8qQ4K.js +0 -56
- package/server/lib/ngrok-tunnel.js +0 -343
- package/server/lib/ngrok-tunnel.test.js +0 -50
- package/server/lib/tunnels/providers/ngrok.js +0 -32
package/server/lib/pi/routes.js
CHANGED
|
@@ -135,6 +135,23 @@ const projectUsage = (raw) => {
|
|
|
135
135
|
};
|
|
136
136
|
};
|
|
137
137
|
|
|
138
|
+
const sanitizeNavigation = (value) => {
|
|
139
|
+
if (!value || typeof value !== 'object' || typeof value.targetEntryId !== 'string' || value.targetEntryId.length === 0) return null;
|
|
140
|
+
const previousLeafId = value.previousLeafId === null ? null : (typeof value.previousLeafId === 'string' ? value.previousLeafId : null);
|
|
141
|
+
const newLeafId = value.newLeafId === null ? null : (typeof value.newLeafId === 'string' ? value.newLeafId : null);
|
|
142
|
+
if (previousLeafId !== null && typeof previousLeafId !== 'string') return null;
|
|
143
|
+
if (newLeafId !== null && typeof newLeafId !== 'string') return null;
|
|
144
|
+
const editorText = typeof value.editorText === 'string' ? value.editorText : undefined;
|
|
145
|
+
// Limit editorText defensively; Pi prompts are bounded and we never persist this.
|
|
146
|
+
if (editorText !== undefined && editorText.length > 200_000) return null;
|
|
147
|
+
return {
|
|
148
|
+
targetEntryId: value.targetEntryId,
|
|
149
|
+
previousLeafId,
|
|
150
|
+
newLeafId,
|
|
151
|
+
...(editorText !== undefined ? { editorText } : {}),
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
138
155
|
const projectSessionDetail = (value) => {
|
|
139
156
|
if (!value || typeof value !== 'object' || !Array.isArray(value.messages) || !Number.isSafeInteger(value.lastSequence)) throw protocolMismatch();
|
|
140
157
|
const messages = value.messages.map((item) => {
|
|
@@ -495,7 +512,6 @@ export const registerPiRuntimeRoutes = (app, {
|
|
|
495
512
|
currentVersion: typeof req.query.currentVersion === 'string' ? req.query.currentVersion : undefined,
|
|
496
513
|
appType: typeof req.query.appType === 'string' ? req.query.appType : undefined,
|
|
497
514
|
platform: typeof req.query.platform === 'string' ? req.query.platform : undefined,
|
|
498
|
-
reportUsage: req.query.reportUsage === 'true',
|
|
499
515
|
}));
|
|
500
516
|
} catch {
|
|
501
517
|
res.status(503).json({ error: 'Update check unavailable' });
|
|
@@ -567,6 +583,16 @@ export const registerPiRuntimeRoutes = (app, {
|
|
|
567
583
|
}
|
|
568
584
|
});
|
|
569
585
|
|
|
586
|
+
app.post('/api/pi/providers/refresh', async (req, res) => {
|
|
587
|
+
const directory = typeof req.body?.directory === 'string' && req.body.directory.length > 0 ? req.body.directory : undefined;
|
|
588
|
+
try {
|
|
589
|
+
const result = await getDaemonRuntime(getPiSessionDaemonRuntime).request('providers.refresh', directory ? { directory } : undefined);
|
|
590
|
+
res.json(projectProviders(result));
|
|
591
|
+
} catch (error) {
|
|
592
|
+
writeDaemonError(res, error);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
570
596
|
app.get('/api/pi/providers/:providerId/config', async (req, res) => {
|
|
571
597
|
const providerId = req.params.providerId;
|
|
572
598
|
if (typeof providerId !== 'string' || providerId.length === 0) {
|
|
@@ -891,7 +917,11 @@ export const registerPiRuntimeRoutes = (app, {
|
|
|
891
917
|
|
|
892
918
|
app.post('/api/pi/sessions/:sessionId/navigate', async (req, res) => {
|
|
893
919
|
const result = await requestSessionOperation(req, res, getPiSessionDaemonRuntime, 'sessions.navigate', { messageId: req.body?.messageId });
|
|
894
|
-
if (result
|
|
920
|
+
if (result === undefined) return;
|
|
921
|
+
const detail = projectSessionDetail(result);
|
|
922
|
+
const navigation = sanitizeNavigation(result.navigation);
|
|
923
|
+
if (navigation) detail.navigation = navigation;
|
|
924
|
+
res.json(detail);
|
|
895
925
|
});
|
|
896
926
|
|
|
897
927
|
for (const [suffix, command] of [['fork', 'sessions.fork'], ['clone', 'sessions.clone']]) {
|
|
@@ -27,7 +27,7 @@ This module is the Pi-owned session-daemon foundation. A detached local daemon p
|
|
|
27
27
|
- Frames are LF-delimited JSON. They have a 1 MiB maximum buffered size. A malformed, oversized, unauthenticated, or invalid request closes the connection.
|
|
28
28
|
- The private core commands are `runtime.health`, `projects.list/select`, `providers.list/config.get/models.set/status/login/login.respond/login.status/logout`, `settings.get/set`, `resources.list/update/prompts.create/prompts.delete`, and session list/create/open/rename/delete/tree/navigate/fork/clone/prompt/steer/followUp/abort/model/thinking/compact operations. Resource lists use opaque IDs and never disclose server paths. Only top-level Pi prompt templates and applicable AGENTS.md sources are editable; package/path resources and skills are browse-only. Resource writes are rejected while a session streams, use an atomic replace, then recreate the idle runtime. Project trust changes are persisted in Pi trust.json and likewise recreate an idle runtime; extensions remain disabled regardless of trust. Provider login submits API keys only across authenticated private IPC or brokers Pi's OAuth/device/manual-code interaction; public projections contain only opaque login IDs, prompts, URLs/device codes, model catalog/configuration, and stable states, never credentials or write-only headers. `providers.models.set` is rejected while a session streams, atomically persists Pi's `models.json`, then rehydrates the idle runtime so the catalog is authoritative immediately. `providers.list` projects only catalog and authentication-status metadata, including Pi-supported `thinkingLevels` (`null` map entries hidden, `xhigh`/`max` opt-in). Session thinking overrides are clamped to the live model's supported levels. `sessions.list` checks each cwd JSONL header (not the full transcript) so a project with large session logs cannot spend tens of seconds on startup. SDK `SessionManager.list` is not used for that path; `listPiSessionJsonlDirectory` reads a header, a bounded prefix for the first user prompt, and a tail for the latest rename. Concurrent lists of the same directory share one in-flight result. Concurrent `sessions.open` of the same id share one in-flight runtime create; a duplicate that finishes later disposes itself and returns the registered runtime instead of `SESSION_RUNTIME_CONFLICT`. Full-file JSONL validation stays on open of a chosen session. The browser explicitly selects an existing user-chosen project directory before listing it; the daemon's startup cwd is an internal fallback, not a user-visible default project. Unnamed persisted sessions use their first prompt as a readable list title when available. `sessions.open` and `getSession` project the last assistant `provider`/`model`/`thinkingLevel` onto the session record so reopening a chat keeps that cache identity. Session selection validates a persisted JSONL path before Pi opens it; inactive mutations are never directed by a browser-supplied path. `sessions.open` resolves an id by Pi's `timestamp_<id>.jsonl` filename first so a missing deep link 404s without listing or fully reading every transcript in every cwd; SDK list and header scan remain the fallback for non-standard names. Directory-wide JSONL checks on list/create/startup are header-only; full-file validation stays on open of a chosen transcript. Attachment paths originate in the server-local upload map and cross only private IPC into Pi's tool context. Pi may persist or echo those paths in its authoritative JSONL transcript, so every public projection redacts them recursively: session titles/previews, user and assistant text/thinking, tool arguments, errors, snapshots, and live message events never expose the server-local path. The daemon maps Pi message lifecycle, deltas, tools, queue changes, model/thinking, compaction, interruption state, and session title updates (`session.updated` on create-with-title, first-prompt auto-title, and rename) to canonical events. Tool events carry redacted input arguments, cumulative partial-output snapshots, final output, error text, renderer metadata (edit diffs, truncation notes), and start/end timestamps. Persisted `toolResult` entries merge into their assistant `toolCall` parts as output/error/end time so hydrated sessions render complete tool cards; tool-result entries never surface as standalone messages. Assistant message projections and start events carry the owning user-message id so the restored turn renderer can group and display replies; all assistant entries in a tool loop remain attached to the latest user turn. Assistant message projections also pass through Pi's per-turn `Usage` (input, output, cacheRead, cacheWrite, totalTokens, and the same five numeric cost fields). The daemon sanitizes the raw payload through `projectUsage` before publication: every numeric must be finite and non-negative, the whole object is omitted when any field is missing or wrong type, and unknown keys are never copied. The `assistant.message.end` event carries the same sanitized usage so the last turn updates without waiting for a full `getSession` refetch. The public route whitelist re-applies the same sanitizer on each projection so a stale or malformed payload from a different daemon version cannot surface as `NaN` or `Infinity` in the reducer.
|
|
29
29
|
- Each event and reconnect snapshot has a monotonic sequence. A reconnect receives a bounded contiguous replay after its cursor when available, otherwise an authoritative `session.snapshot`; unavailable runtimes are not represented as empty sessions.
|
|
30
|
-
- Prompt, steer, and follow-up requests acknowledge acceptance immediately after Pi accepts the call; the daemon owns the still-running send promise and publishes any asynchronous rejection as `session.error`. A later prompt on the same session invalidates that rejection so it cannot error the new turn. If the rejected send left Pi streaming, the daemon aborts it. Steer/follow-up against an idle session starts a new prompt instead of `SESSION_NOT_RUNNING`. Agent completion is never held behind the 30-second public/private request budgets.
|
|
30
|
+
- Prompt, steer, and follow-up requests acknowledge acceptance immediately after Pi accepts the call; the daemon owns the still-running send promise and publishes any asynchronous rejection as `session.error`. A later prompt on the same session invalidates that rejection so it cannot error the new turn. If the rejected send left Pi streaming, the daemon aborts it. Steer/follow-up against an idle session starts a new prompt instead of `SESSION_NOT_RUNNING`. Agent completion is never held behind the 30-second public/private request budgets. Pi retryable provider failures remain live: an `agent_end` with `willRetry` does not publish terminal `session.error`, and `auto_retry_start` projects Pi's attempt, next-attempt time, and redacted error text through `session.lifecycle: retry`. `agent_settled` remains the authoritative idle boundary after retry success, exhaustion, or cancellation.
|
|
31
31
|
- Registry keys combine the SDK session ID with its cwd. A replacement subscription is rebound to the replacement session so old-session events cannot be published under the new identity. A collision is a visible error; it never replaces or disposes an unrelated runtime.
|
|
32
32
|
- After `agent_settled`, the daemon disposes an idle runtime after five minutes (injectable for tests). Disposal never deletes Pi JSONL; the daemon remembers the selected session file and recreates that runtime on the next private prompt. A new agent start cancels a pending disposal.
|
|
33
33
|
- A startup validation failure writes only a stable failure code to the owner-only non-secret state sidecar. It never records a transcript, session path, or credential. The supervisor surfaces that code as unavailable instead of claiming an empty or idle session.
|
|
@@ -140,6 +140,9 @@ export function createSessionRuntimeRegistry({ onSessionEvent = () => {} } = {})
|
|
|
140
140
|
register,
|
|
141
141
|
dispose,
|
|
142
142
|
disposeAll,
|
|
143
|
+
listAll() {
|
|
144
|
+
return [...recordsByKey.values()].map((record) => record.runtime);
|
|
145
|
+
},
|
|
143
146
|
get size() {
|
|
144
147
|
return recordsByKey.size;
|
|
145
148
|
},
|
|
@@ -631,7 +631,11 @@ export function createSessionDaemon({
|
|
|
631
631
|
};
|
|
632
632
|
|
|
633
633
|
const projectMessageEntries = (session, targetDir = activeDirectory || cwd) => {
|
|
634
|
-
|
|
634
|
+
// Use the active branch, not the full file. `getEntries()` returns every
|
|
635
|
+
// entry ever written, so a bare `branch()`/`resetLeaf()` would appear to
|
|
636
|
+
// do nothing. `getBranch()` follows the current leaf and is what
|
|
637
|
+
// `navigateTree` and `buildSessionContext` use for the model context.
|
|
638
|
+
const persisted = session?.sessionManager?.getBranch?.() ?? session?.sessionManager?.getEntries?.();
|
|
635
639
|
const entries = liveProjectionEntries(session, Array.isArray(persisted) ? persisted : []);
|
|
636
640
|
if (entries.length === 0) return [];
|
|
637
641
|
const streaming = session?.isStreaming === true;
|
|
@@ -820,6 +824,84 @@ export function createSessionDaemon({
|
|
|
820
824
|
return { providers: [...providers.values()] };
|
|
821
825
|
};
|
|
822
826
|
|
|
827
|
+
let refreshProvidersInflight = null;
|
|
828
|
+
const refreshProviders = async (requestedDirectory) => {
|
|
829
|
+
if (refreshProvidersInflight) return refreshProvidersInflight;
|
|
830
|
+
const task = (async () => {
|
|
831
|
+
const signal = typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(15_000) : undefined;
|
|
832
|
+
const runtimes = new Set();
|
|
833
|
+
for (const services of servicesCache.values()) {
|
|
834
|
+
const mr = services?.modelRuntime;
|
|
835
|
+
if (mr) runtimes.add(mr);
|
|
836
|
+
}
|
|
837
|
+
if (runtime?.session?.modelRuntime) runtimes.add(runtime.session.modelRuntime);
|
|
838
|
+
if (runtime?.services?.modelRuntime) runtimes.add(runtime.services.modelRuntime);
|
|
839
|
+
if (runtimeRegistry?.listAll) {
|
|
840
|
+
try {
|
|
841
|
+
for (const tracked of runtimeRegistry.listAll()) {
|
|
842
|
+
const mr = tracked?.session?.modelRuntime ?? tracked?.services?.modelRuntime;
|
|
843
|
+
if (mr) runtimes.add(mr);
|
|
844
|
+
}
|
|
845
|
+
} catch {}
|
|
846
|
+
}
|
|
847
|
+
if (runtimes.size === 0) {
|
|
848
|
+
const active = await ensureRuntime(requestedDirectory ? await resolveDirectory(requestedDirectory) : undefined);
|
|
849
|
+
const mr = active?.session?.modelRuntime ?? active?.services?.modelRuntime;
|
|
850
|
+
if (mr) runtimes.add(mr);
|
|
851
|
+
}
|
|
852
|
+
const dummyMap = new Map();
|
|
853
|
+
for (const mr of runtimes) {
|
|
854
|
+
try {
|
|
855
|
+
const providers = typeof mr.getProviders === 'function' ? mr.getProviders() : [];
|
|
856
|
+
for (const provider of providers) {
|
|
857
|
+
const auth = mr.getProviderAuthStatus?.(provider.id);
|
|
858
|
+
if (auth?.configured === true) continue;
|
|
859
|
+
try {
|
|
860
|
+
await mr.setRuntimeApiKey(provider.id, 'pichamber-catalog-refresh');
|
|
861
|
+
let list = dummyMap.get(mr);
|
|
862
|
+
if (!list) { list = []; dummyMap.set(mr, list); }
|
|
863
|
+
list.push(provider.id);
|
|
864
|
+
} catch {}
|
|
865
|
+
}
|
|
866
|
+
} catch {}
|
|
867
|
+
}
|
|
868
|
+
const errors = new Map();
|
|
869
|
+
let aborted = false;
|
|
870
|
+
try {
|
|
871
|
+
await Promise.all([...runtimes].map(async (mr) => {
|
|
872
|
+
try {
|
|
873
|
+
const result = await mr.refresh({ allowNetwork: true, force: true, ...(signal ? { signal } : {}) });
|
|
874
|
+
if (result?.aborted) aborted = true;
|
|
875
|
+
if (result?.errors) {
|
|
876
|
+
for (const [providerId, err] of result.errors) errors.set(providerId, err);
|
|
877
|
+
}
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (error?.code === 'PI_MODEL_CONFIG_INVALID') throw error;
|
|
880
|
+
errors.set('_global', error);
|
|
881
|
+
}
|
|
882
|
+
}));
|
|
883
|
+
} finally {
|
|
884
|
+
for (const [mr, ids] of dummyMap.entries()) {
|
|
885
|
+
for (const id of ids) {
|
|
886
|
+
try { await mr.removeRuntimeApiKey(id); } catch {}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (errors.has('_global') && runtimes.size > 0) {
|
|
891
|
+
const globalError = errors.get('_global');
|
|
892
|
+
if (globalError?.code === 'PI_MODEL_CONFIG_INVALID') {
|
|
893
|
+
throw new SessionDaemonProtocolError('PI_MODEL_CONFIG_INVALID', 'Pi models configuration is invalid.');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const catalog = await listProviders(requestedDirectory);
|
|
897
|
+
return catalog;
|
|
898
|
+
})().finally(() => {
|
|
899
|
+
refreshProvidersInflight = null;
|
|
900
|
+
});
|
|
901
|
+
refreshProvidersInflight = task;
|
|
902
|
+
return task;
|
|
903
|
+
};
|
|
904
|
+
|
|
823
905
|
const getProviderConfiguration = async (providerId) => {
|
|
824
906
|
if (typeof providerId !== 'string' || providerId.length === 0 || providerId.length > 256) {
|
|
825
907
|
throw new SessionDaemonProtocolError('INVALID_ARGUMENT', 'The requested provider is invalid.');
|
|
@@ -1101,9 +1183,21 @@ export function createSessionDaemon({
|
|
|
1101
1183
|
if (!loader || typeof loader.getSkills !== 'function' || typeof loader.getPrompts !== 'function' || typeof loader.getAgentsFiles !== 'function') {
|
|
1102
1184
|
throw new SessionDaemonProtocolError('DAEMON_REQUEST_FAILED', 'Pi resource discovery is unavailable.');
|
|
1103
1185
|
}
|
|
1104
|
-
const skills = loader.getSkills().skills.map((skill) =>
|
|
1105
|
-
|
|
1106
|
-
|
|
1186
|
+
const skills = await Promise.all(loader.getSkills().skills.map(async (skill) => {
|
|
1187
|
+
let content = '';
|
|
1188
|
+
try {
|
|
1189
|
+
content = await readFile(skill.filePath, 'utf8');
|
|
1190
|
+
} catch {}
|
|
1191
|
+
return {
|
|
1192
|
+
id: resourceId('skill', skill.filePath),
|
|
1193
|
+
kind: 'skill',
|
|
1194
|
+
name: skill.name,
|
|
1195
|
+
...(skill.description ? { description: skill.description } : {}),
|
|
1196
|
+
location: resourceLocation(skill.sourceInfo),
|
|
1197
|
+
editable: false,
|
|
1198
|
+
...(content ? { content } : {}),
|
|
1199
|
+
filePath: skill.filePath,
|
|
1200
|
+
};
|
|
1107
1201
|
}));
|
|
1108
1202
|
const prompts = loader.getPrompts().prompts.map((prompt) => ({
|
|
1109
1203
|
id: resourceId('prompt', prompt.filePath),
|
|
@@ -1642,11 +1736,19 @@ export function createSessionDaemon({
|
|
|
1642
1736
|
const finalMessage = event.messages?.at?.(-1);
|
|
1643
1737
|
if (finalMessage?.role === 'assistant' && finalMessage.stopReason === 'aborted') {
|
|
1644
1738
|
publish('session.interrupted', { reason: 'user-abort', streaming: false }, sessionId, directory);
|
|
1645
|
-
} else if (finalMessage?.role === 'assistant' && typeof finalMessage.errorMessage === 'string') {
|
|
1739
|
+
} else if (finalMessage?.role === 'assistant' && typeof finalMessage.errorMessage === 'string' && event.willRetry !== true) {
|
|
1646
1740
|
publish('session.error', { code: 'ASSISTANT_ERROR', message: redactAttachmentPaths(finalMessage.errorMessage) }, sessionId, directory);
|
|
1647
1741
|
}
|
|
1648
1742
|
break;
|
|
1649
1743
|
}
|
|
1744
|
+
case 'auto_retry_start':
|
|
1745
|
+
publish('session.lifecycle', {
|
|
1746
|
+
state: 'retry',
|
|
1747
|
+
attempt: event.attempt,
|
|
1748
|
+
next: Date.now() + event.delayMs,
|
|
1749
|
+
message: redactAttachmentPaths(event.errorMessage),
|
|
1750
|
+
}, sessionId, directory);
|
|
1751
|
+
break;
|
|
1650
1752
|
case 'agent_settled':
|
|
1651
1753
|
publish('session.lifecycle', { state: 'idle' }, sessionId, directory);
|
|
1652
1754
|
scheduleIdleDisposal(sessionId);
|
|
@@ -1684,7 +1786,7 @@ export function createSessionDaemon({
|
|
|
1684
1786
|
'projects.list', 'projects.select', 'sessions.list', 'sessions.create', 'sessions.open', 'sessions.rename', 'sessions.delete',
|
|
1685
1787
|
'sessions.tree', 'sessions.navigate', 'sessions.fork', 'sessions.clone', 'sessions.prompt',
|
|
1686
1788
|
'sessions.steer', 'sessions.followUp', 'sessions.abort', 'sessions.setModel',
|
|
1687
|
-
'sessions.setThinking', 'sessions.compact', 'providers.list', 'providers.config.get', 'providers.models.set', 'providers.status', 'providers.login',
|
|
1789
|
+
'sessions.setThinking', 'sessions.compact', 'providers.list', 'providers.refresh', 'providers.config.get', 'providers.models.set', 'providers.status', 'providers.login',
|
|
1688
1790
|
'providers.login.respond', 'providers.login.status', 'providers.logout', 'settings.get', 'settings.set',
|
|
1689
1791
|
'resources.list', 'resources.update', 'resources.prompts.create', 'resources.prompts.delete',
|
|
1690
1792
|
],
|
|
@@ -1716,6 +1818,11 @@ export function createSessionDaemon({
|
|
|
1716
1818
|
writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result });
|
|
1717
1819
|
return;
|
|
1718
1820
|
}
|
|
1821
|
+
case 'providers.refresh': {
|
|
1822
|
+
const result = await refreshProviders(message.payload?.directory);
|
|
1823
|
+
writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result });
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1719
1826
|
case 'providers.config.get': {
|
|
1720
1827
|
const result = await getProviderConfiguration(message.payload?.providerId);
|
|
1721
1828
|
writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result });
|
|
@@ -1817,11 +1924,44 @@ export function createSessionDaemon({
|
|
|
1817
1924
|
}
|
|
1818
1925
|
case 'sessions.navigate': {
|
|
1819
1926
|
const activeRuntime = await activateSession(message.payload?.sessionId, message.payload?.directory || message.payload?.cwd);
|
|
1820
|
-
|
|
1927
|
+
let messageId = message.payload?.messageId;
|
|
1821
1928
|
if (typeof messageId !== 'string' || messageId.length === 0) throw new SessionDaemonProtocolError('INVALID_ARGUMENT', 'The requested tree entry is invalid.');
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1929
|
+
// UI may pass a part id like "c865141a:text:1" (e.g. from a tool block).
|
|
1930
|
+
// Pi only knows entry ids, so strip the suffix and retry the entry lookup.
|
|
1931
|
+
const tryIds = [messageId];
|
|
1932
|
+
if (messageId.includes(':')) {
|
|
1933
|
+
const entryId = messageId.split(':')[0];
|
|
1934
|
+
if (entryId && entryId !== messageId) tryIds.unshift(entryId);
|
|
1935
|
+
}
|
|
1936
|
+
const previousLeafId = activeRuntime.session.sessionManager?.getLeafId?.() ?? null;
|
|
1937
|
+
let result;
|
|
1938
|
+
let lastError;
|
|
1939
|
+
for (const tryId of tryIds) {
|
|
1940
|
+
try {
|
|
1941
|
+
result = await activeRuntime.session.navigateTree(tryId);
|
|
1942
|
+
messageId = tryId;
|
|
1943
|
+
lastError = undefined;
|
|
1944
|
+
break;
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
lastError = error;
|
|
1947
|
+
// Only retry on "Entry not found" with a stripped id; otherwise surface immediately.
|
|
1948
|
+
const isEntryNotFound = String(error?.message ?? '').includes('not found');
|
|
1949
|
+
if (!isEntryNotFound || tryId === tryIds[tryIds.length - 1]) {
|
|
1950
|
+
console.error(`[pi:navigate] target=${tryId} leaf=${previousLeafId} branch=${activeRuntime.session.sessionManager?.getBranch?.()?.map((e) => e.id).join(',') ?? 'unknown'} error=${error?.message ?? String(error)}`);
|
|
1951
|
+
throw error;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if (lastError) throw lastError;
|
|
1956
|
+
if (result?.cancelled) throw new SessionDaemonProtocolError('SESSION_TREE_NOT_FOUND', 'Pi cancelled tree navigation.');
|
|
1957
|
+
const newLeafId = activeRuntime.session.sessionManager?.getLeafId?.() ?? null;
|
|
1958
|
+
const navigation = {
|
|
1959
|
+
targetEntryId: messageId,
|
|
1960
|
+
previousLeafId: typeof previousLeafId === 'string' ? previousLeafId : null,
|
|
1961
|
+
newLeafId: typeof newLeafId === 'string' ? newLeafId : null,
|
|
1962
|
+
...(typeof result?.editorText === 'string' && result.editorText.length > 0 ? { editorText: result.editorText } : {}),
|
|
1963
|
+
};
|
|
1964
|
+
writeFrame(socket, { protocolVersion: PROTOCOL_VERSION, kind: 'response', requestId: message.requestId, result: { ...projectActiveSession(activeRuntime, activeRuntime.cwd), navigation } });
|
|
1825
1965
|
return;
|
|
1826
1966
|
}
|
|
1827
1967
|
case 'sessions.fork':
|
|
@@ -971,6 +971,28 @@ describe('Pi session daemon spike', () => {
|
|
|
971
971
|
await client.close();
|
|
972
972
|
});
|
|
973
973
|
|
|
974
|
+
it('keeps retryable provider failures in retry lifecycle until Pi exhausts retry', async () => {
|
|
975
|
+
const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-retry-'));
|
|
976
|
+
const endpoint = testDaemonEndpoint(root);
|
|
977
|
+
const session = new FakeSession('pi-session-retry');
|
|
978
|
+
daemon = createSessionDaemon({ endpoint, credential, cwd: root, createRuntime: async () => ({ session, async dispose() {} }) });
|
|
979
|
+
await daemon.start();
|
|
980
|
+
const client = connectClient(endpoint);
|
|
981
|
+
await client.authenticate();
|
|
982
|
+
await client.request('sessions.create', { cwd: root });
|
|
983
|
+
|
|
984
|
+
const retryLifecycle = client.next((frame) => frame.event === 'session.lifecycle' && frame.payload.state === 'retry');
|
|
985
|
+
const prematureError = client.next((frame) => frame.event === 'session.error');
|
|
986
|
+
session.emit({ type: 'agent_end', willRetry: true, messages: [{ role: 'assistant', stopReason: 'error', errorMessage: 'Rate limit exceeded' }] });
|
|
987
|
+
session.emit({ type: 'auto_retry_start', attempt: 1, maxAttempts: 3, delayMs: 2_000, errorMessage: 'Rate limit exceeded' });
|
|
988
|
+
|
|
989
|
+
await expect(retryLifecycle).resolves.toMatchObject({
|
|
990
|
+
payload: { state: 'retry', attempt: 1, message: 'Rate limit exceeded' },
|
|
991
|
+
});
|
|
992
|
+
await expect(prematureError).rejects.toThrow(/Timed out/);
|
|
993
|
+
await client.close();
|
|
994
|
+
});
|
|
995
|
+
|
|
974
996
|
it('preserves assistant messageId for tool executions occurring after message_end', async () => {
|
|
975
997
|
const root = await mkdtemp(join(tmpdir(), 'pichamber-pi-daemon-'));
|
|
976
998
|
const endpoint = testDaemonEndpoint(root);
|
|
@@ -5,16 +5,15 @@ This module contains the shared tunnel support surface for PiChamber: provider
|
|
|
5
5
|
capability metadata, tunnel types/normalization helpers, cross-platform
|
|
6
6
|
executable discovery, and dependency install-help metadata.
|
|
7
7
|
|
|
8
|
-
The
|
|
9
|
-
`packages/web/server/lib/cloudflare-tunnel.js
|
|
10
|
-
`packages/web/server/lib/ngrok-tunnel.js`; CLI orchestration lives in
|
|
8
|
+
The tunnel start implementation lives in
|
|
9
|
+
`packages/web/server/lib/cloudflare-tunnel.js`; CLI orchestration lives in
|
|
11
10
|
`packages/web/bin/lib/commands-tunnel.js`.
|
|
12
11
|
|
|
13
12
|
> Wiring note: the previous service orchestration entrypoint
|
|
14
13
|
> (`tunnels/index.js`, `createTunnelService`) was removed as dead code — no
|
|
15
14
|
> runtime entrypoint registered tunnel routes or started a tunnel service.
|
|
16
15
|
> The remaining files are consumed by the CLI capability surface and by the
|
|
17
|
-
> tunnel provider
|
|
16
|
+
> Cloudflare tunnel provider implementation, not by a tunnel orchestration runtime.
|
|
18
17
|
|
|
19
18
|
## Entrypoints and structure
|
|
20
19
|
- `packages/web/server/lib/tunnels/executable-search.js`: cross-platform executable discovery, including Windows Store app aliases. Consumed by the tunnel providers and `packages/web/server/lib/workspace/host.js`.
|
|
@@ -24,10 +23,10 @@ The actual tunnel start implementations live in
|
|
|
24
23
|
## Public exports
|
|
25
24
|
- `install-help.js` → `getTunnelDependencyInstallInfo(provider, platform)`
|
|
26
25
|
- `executable-search.js` → `getExecutableSearchDirectories`, `createExecutableSearchEnv`, `findExecutableOnPath`, `resolveExecutableLaunchTarget`
|
|
27
|
-
- `types.js` → `TUNNEL_PROVIDER_CLOUDFLARE`, `
|
|
26
|
+
- `types.js` → `TUNNEL_PROVIDER_CLOUDFLARE`, `TUNNEL_MODE_QUICK`, `TUNNEL_MODE_MANAGED_REMOTE`, `TUNNEL_MODE_MANAGED_LOCAL`, `TUNNEL_INTENT_EPHEMERAL_PUBLIC`, `TUNNEL_INTENT_PERSISTENT_PUBLIC`, `TunnelServiceError`
|
|
28
27
|
|
|
29
28
|
## Consumers
|
|
30
|
-
- `packages/web/bin/lib/cli-tunnel-capabilities.js`: provider capability metadata for CLI tunnel commands.
|
|
31
|
-
- `packages/web/server/lib/cloudflare-tunnel.js
|
|
29
|
+
- `packages/web/bin/lib/cli-tunnel-capabilities.js`: provider capability metadata for CLI tunnel commands (Cloudflare only).
|
|
30
|
+
- `packages/web/server/lib/cloudflare-tunnel.js`: executable discovery, install-help metadata, and provider constants.
|
|
32
31
|
- `packages/web/server/lib/workspace/host.js`: executable discovery.
|
|
33
32
|
|
|
@@ -36,7 +36,7 @@ describe('getExecutableSearchDirectories', () => {
|
|
|
36
36
|
|
|
37
37
|
describe('findExecutableOnPath', () => {
|
|
38
38
|
it('finds Windows Store app execution aliases even when PATH omits WindowsApps', () => {
|
|
39
|
-
const aliasPath = 'C:\\Users\\Ada\\AppData\\Local\\Microsoft\\WindowsApps\\
|
|
39
|
+
const aliasPath = 'C:\\Users\\Ada\\AppData\\Local\\Microsoft\\WindowsApps\\cloudflared.exe';
|
|
40
40
|
const fsLike = {
|
|
41
41
|
statSync: (candidate) => {
|
|
42
42
|
if (candidate === aliasPath) {
|
|
@@ -47,7 +47,7 @@ describe('findExecutableOnPath', () => {
|
|
|
47
47
|
accessSync: () => {},
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
const resolved = findExecutableOnPath('
|
|
50
|
+
const resolved = findExecutableOnPath('cloudflared', {
|
|
51
51
|
platform: 'win32',
|
|
52
52
|
env: {
|
|
53
53
|
PATH: 'C:\\Tools',
|
|
@@ -63,7 +63,7 @@ describe('findExecutableOnPath', () => {
|
|
|
63
63
|
|
|
64
64
|
describe('resolveExecutableLaunchTarget', () => {
|
|
65
65
|
it('returns a Windows launch target with WindowsApps on PATH when stat lookup fails', () => {
|
|
66
|
-
const target = resolveExecutableLaunchTarget('
|
|
66
|
+
const target = resolveExecutableLaunchTarget('cloudflared', {
|
|
67
67
|
platform: 'win32',
|
|
68
68
|
env: {
|
|
69
69
|
PATH: 'C:\\Windows\\System32',
|
|
@@ -75,7 +75,7 @@ describe('resolveExecutableLaunchTarget', () => {
|
|
|
75
75
|
},
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
expect(target?.command).toBe('
|
|
78
|
+
expect(target?.command).toBe('cloudflared');
|
|
79
79
|
expect(target?.env.Path).toContain('C:\\Users\\Ada\\AppData\\Local\\Microsoft\\WindowsApps');
|
|
80
80
|
});
|
|
81
81
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TUNNEL_PROVIDER_CLOUDFLARE,
|
|
3
|
-
TUNNEL_PROVIDER_NGROK,
|
|
4
3
|
} from './types.js';
|
|
5
4
|
|
|
6
5
|
const PROVIDER_INSTALL_INFO = {
|
|
@@ -13,15 +12,6 @@ const PROVIDER_INSTALL_INFO = {
|
|
|
13
12
|
linux: 'Download cloudflared from https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflared/downloads/',
|
|
14
13
|
},
|
|
15
14
|
},
|
|
16
|
-
[TUNNEL_PROVIDER_NGROK]: {
|
|
17
|
-
dependency: 'ngrok',
|
|
18
|
-
installUrl: 'https://ngrok.com/download',
|
|
19
|
-
commands: {
|
|
20
|
-
darwin: 'brew install ngrok',
|
|
21
|
-
win32: 'winget install ngrok -s msstore',
|
|
22
|
-
linux: 'Download ngrok from https://ngrok.com/download',
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
15
|
};
|
|
26
16
|
|
|
27
17
|
const normalizeInstallPlatform = (platform) => {
|
|
@@ -3,7 +3,6 @@ import { describe, expect, it } from 'bun:test';
|
|
|
3
3
|
import { getTunnelDependencyInstallInfo } from './install-help.js';
|
|
4
4
|
import {
|
|
5
5
|
TUNNEL_PROVIDER_CLOUDFLARE,
|
|
6
|
-
TUNNEL_PROVIDER_NGROK,
|
|
7
6
|
} from './types.js';
|
|
8
7
|
|
|
9
8
|
describe('getTunnelDependencyInstallInfo', () => {
|
|
@@ -15,14 +14,6 @@ describe('getTunnelDependencyInstallInfo', () => {
|
|
|
15
14
|
expect(info.message).toContain('Cloudflare.cloudflared');
|
|
16
15
|
});
|
|
17
16
|
|
|
18
|
-
it('returns Windows ngrok winget guidance', () => {
|
|
19
|
-
const info = getTunnelDependencyInstallInfo(TUNNEL_PROVIDER_NGROK, 'win32');
|
|
20
|
-
|
|
21
|
-
expect(info.dependency).toBe('ngrok');
|
|
22
|
-
expect(info.installCommand).toBe('winget install ngrok -s msstore');
|
|
23
|
-
expect(info.message).toContain('ngrok -s msstore');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
17
|
it('keeps macOS Homebrew guidance', () => {
|
|
27
18
|
const info = getTunnelDependencyInstallInfo(TUNNEL_PROVIDER_CLOUDFLARE, 'darwin');
|
|
28
19
|
|