@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { createJiti } from "jiti";
|
|
7
|
+
|
|
8
|
+
const jiti = createJiti(import.meta.url, {
|
|
9
|
+
interopDefault: true,
|
|
10
|
+
moduleCache: false,
|
|
11
|
+
});
|
|
12
|
+
const { getRpcSessionInfos } = await jiti.import("./rpc-manager.ts");
|
|
13
|
+
|
|
14
|
+
function makeRuntimeSession({ id, filePath, running, entries }) {
|
|
15
|
+
const timestamp = "2026-08-12T01:02:03.000Z";
|
|
16
|
+
const manager = {
|
|
17
|
+
getHeader: () => ({ type: "session", id, cwd: "/tmp/runtime-cwd", timestamp }),
|
|
18
|
+
getEntries: () => entries,
|
|
19
|
+
getSessionFile: () => filePath,
|
|
20
|
+
getSessionName: () => undefined,
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
isAlive: () => true,
|
|
24
|
+
isRunning: () => running,
|
|
25
|
+
inner: { sessionManager: manager },
|
|
26
|
+
get sessionId() { return id; },
|
|
27
|
+
get sessionFile() { return filePath; },
|
|
28
|
+
get cwd() { return "/tmp/runtime-cwd"; },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("lists an accepted new prompt before its session file exists", (t) => {
|
|
33
|
+
const previousRegistry = globalThis.__piSessions;
|
|
34
|
+
const timestamp = "2026-08-12T01:02:04.000Z";
|
|
35
|
+
const visible = makeRuntimeSession({
|
|
36
|
+
id: "visible-runtime",
|
|
37
|
+
filePath: join(tmpdir(), "pi-web-missing-runtime-session.jsonl"),
|
|
38
|
+
running: true,
|
|
39
|
+
entries: [{
|
|
40
|
+
type: "message",
|
|
41
|
+
id: "u1",
|
|
42
|
+
parentId: null,
|
|
43
|
+
timestamp,
|
|
44
|
+
message: {
|
|
45
|
+
role: "user",
|
|
46
|
+
content: [{ type: "text", text: "first" }, { type: "text", text: "prompt" }],
|
|
47
|
+
},
|
|
48
|
+
}],
|
|
49
|
+
});
|
|
50
|
+
const emptyEnsureSession = makeRuntimeSession({
|
|
51
|
+
id: "empty-runtime",
|
|
52
|
+
filePath: join(tmpdir(), "pi-web-missing-empty-session.jsonl"),
|
|
53
|
+
running: false,
|
|
54
|
+
entries: [],
|
|
55
|
+
});
|
|
56
|
+
globalThis.__piSessions = new Map([
|
|
57
|
+
["visible-runtime", visible],
|
|
58
|
+
["empty-runtime", emptyEnsureSession],
|
|
59
|
+
]);
|
|
60
|
+
t.after(() => {
|
|
61
|
+
globalThis.__piSessions = previousRegistry;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const infos = getRpcSessionInfos();
|
|
65
|
+
|
|
66
|
+
assert.equal(infos.length, 1);
|
|
67
|
+
assert.equal(infos[0].id, "visible-runtime");
|
|
68
|
+
assert.equal(infos[0].firstMessage, "first prompt");
|
|
69
|
+
assert.equal(infos[0].messageCount, 1);
|
|
70
|
+
assert.equal(infos[0].transient, true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("keeps an idle runtime visible once its JSONL file exists", (t) => {
|
|
74
|
+
const previousRegistry = globalThis.__piSessions;
|
|
75
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-web-runtime-session-"));
|
|
76
|
+
const filePath = join(dir, "session.jsonl");
|
|
77
|
+
writeFileSync(filePath, "persisted\n");
|
|
78
|
+
globalThis.__piSessions = new Map([["persisted-runtime", makeRuntimeSession({
|
|
79
|
+
id: "persisted-runtime",
|
|
80
|
+
filePath,
|
|
81
|
+
running: false,
|
|
82
|
+
entries: [],
|
|
83
|
+
})]]);
|
|
84
|
+
t.after(() => {
|
|
85
|
+
globalThis.__piSessions = previousRegistry;
|
|
86
|
+
rmSync(dir, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const infos = getRpcSessionInfos();
|
|
90
|
+
|
|
91
|
+
assert.equal(infos.length, 1);
|
|
92
|
+
assert.equal(infos[0].transient, false);
|
|
93
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { createJiti } from "jiti";
|
|
7
|
+
|
|
8
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
9
|
+
const { collectSessionDescendants } = await jiti.import("./session-delete.ts");
|
|
10
|
+
const { cacheSessionPath } = await jiti.import("./session-reader.ts");
|
|
11
|
+
|
|
12
|
+
function sessionFile(dir, id, parentPath) {
|
|
13
|
+
const p = join(dir, `${Date.now()}_${id}.jsonl`);
|
|
14
|
+
const header = {
|
|
15
|
+
type: "session",
|
|
16
|
+
version: 3,
|
|
17
|
+
id,
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
cwd: dir,
|
|
20
|
+
...(parentPath ? { parentSession: parentPath } : {}),
|
|
21
|
+
};
|
|
22
|
+
writeFileSync(p, JSON.stringify(header) + "\n");
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("collectSessionDescendants walks the whole fork tree (multi-level)", async () => {
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-sd-"));
|
|
28
|
+
try {
|
|
29
|
+
const rootPath = sessionFile(dir, "root-id");
|
|
30
|
+
const child1Path = sessionFile(dir, "child1", rootPath);
|
|
31
|
+
sessionFile(dir, "child2", child1Path); // 两级分叉:child1 → child2
|
|
32
|
+
sessionFile(dir, "unrelated", undefined); // 无 parentSession 的无关会话
|
|
33
|
+
// 注入路径缓存,resolveSessionPath 命中后不触碰真实 sessions 目录。
|
|
34
|
+
cacheSessionPath("root-id", rootPath);
|
|
35
|
+
|
|
36
|
+
const ids = await collectSessionDescendants("root-id");
|
|
37
|
+
assert.deepEqual(ids.sort(), ["root-id", "child1", "child2"].sort());
|
|
38
|
+
assert.ok(!ids.includes("unrelated"));
|
|
39
|
+
} finally {
|
|
40
|
+
rmSync(dir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("collectSessionDescendants returns the root alone when there are no forks", async () => {
|
|
45
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-sd-"));
|
|
46
|
+
try {
|
|
47
|
+
const rootPath = sessionFile(dir, "root-alone");
|
|
48
|
+
cacheSessionPath("root-alone", rootPath);
|
|
49
|
+
assert.deepEqual(await collectSessionDescendants("root-alone"), ["root-alone"]);
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// 会话文件树删除 —— 供「删除任务 = 删除任务下所有会话(含 fork 子树)」使用。
|
|
2
|
+
//
|
|
3
|
+
// 与单个会话删除(app/api/sessions/[id] DELETE)不同:那里删一个节点并把
|
|
4
|
+
// 子树级联重挂到父级;这里整棵树全部删除,不做重挂。
|
|
5
|
+
|
|
6
|
+
import { readdirSync, unlinkSync } from "fs";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import {
|
|
9
|
+
invalidateSessionListCache,
|
|
10
|
+
invalidateSessionPathCache,
|
|
11
|
+
readSessionHeader,
|
|
12
|
+
resolveSessionPath,
|
|
13
|
+
} from "./session-reader";
|
|
14
|
+
import { sessionPathKey } from "./session-path";
|
|
15
|
+
import { getRpcSession } from "./rpc-manager";
|
|
16
|
+
import { unassignSession } from "./task-store";
|
|
17
|
+
import { removeSessionFromBoards } from "./board-store";
|
|
18
|
+
|
|
19
|
+
/** 递归收集一个会话的全部 fork 后代 id(含自身)。 */
|
|
20
|
+
export async function collectSessionDescendants(rootId: string): Promise<string[]> {
|
|
21
|
+
const rootPath = await resolveSessionPath(rootId);
|
|
22
|
+
if (!rootPath) return [rootId];
|
|
23
|
+
const dir = dirname(rootPath);
|
|
24
|
+
const keySet = new Set<string>([sessionPathKey(rootPath)]);
|
|
25
|
+
const ids = [rootId];
|
|
26
|
+
let files: string[];
|
|
27
|
+
try {
|
|
28
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
29
|
+
} catch {
|
|
30
|
+
return [rootId];
|
|
31
|
+
}
|
|
32
|
+
// 迭代扫描:parentSession 指向已收集集合的 .jsonl 全部纳入(支持多级分叉)。
|
|
33
|
+
let added = true;
|
|
34
|
+
while (added) {
|
|
35
|
+
added = false;
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const p = join(dir, file);
|
|
38
|
+
const pk = sessionPathKey(p);
|
|
39
|
+
if (keySet.has(pk)) continue;
|
|
40
|
+
const header = readSessionHeader(p);
|
|
41
|
+
if (header?.parentSession && keySet.has(sessionPathKey(header.parentSession))) {
|
|
42
|
+
keySet.add(pk);
|
|
43
|
+
ids.push(header.id);
|
|
44
|
+
added = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return ids;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 删除单个会话文件(含 RPC/路径缓存/列表缓存/任务元数据/画布引用),不重挂子树。 */
|
|
52
|
+
export async function deleteSessionFile(id: string): Promise<void> {
|
|
53
|
+
// 画布引用清理(断 exec 线/清任务卡/删节点):无文件也执行,幂等;
|
|
54
|
+
// 任务整树删除时任务看板已由 deleteBoardCascade 清,此处对手动看板残留兜底。
|
|
55
|
+
removeSessionFromBoards(id);
|
|
56
|
+
const filePath = await resolveSessionPath(id);
|
|
57
|
+
if (!filePath) return;
|
|
58
|
+
await getRpcSession(id)?.shutdown();
|
|
59
|
+
try {
|
|
60
|
+
unlinkSync(filePath); // 文件已被并发删除/缓存残留时忽略(健壮删除)
|
|
61
|
+
} catch { /* ignore */ }
|
|
62
|
+
invalidateSessionPathCache(id);
|
|
63
|
+
invalidateSessionListCache();
|
|
64
|
+
unassignSession(id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 删除一组会话(每个根会话连同其 fork 子树),返回实际删除的全部会话 id。
|
|
69
|
+
* 单个文件失败不中断其余删除。
|
|
70
|
+
*/
|
|
71
|
+
export async function deleteSessionTrees(rootIds: string[]): Promise<string[]> {
|
|
72
|
+
const seen = new Set<string>();
|
|
73
|
+
const deleted: string[] = [];
|
|
74
|
+
for (const rootId of rootIds) {
|
|
75
|
+
const ids = await collectSessionDescendants(rootId);
|
|
76
|
+
for (const id of ids) {
|
|
77
|
+
if (seen.has(id)) continue;
|
|
78
|
+
seen.add(id);
|
|
79
|
+
try {
|
|
80
|
+
await deleteSessionFile(id);
|
|
81
|
+
deleted.push(id);
|
|
82
|
+
} catch { /* keep deleting the rest */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return deleted;
|
|
86
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { SessionEntry } from "./types";
|
|
2
|
+
|
|
3
|
+
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
|
|
5
|
+
export function isValidSessionId(sessionId: string | null): sessionId is string {
|
|
6
|
+
return !!sessionId && SESSION_ID_RE.test(sessionId);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function safeDecode(value: string): string {
|
|
10
|
+
try {
|
|
11
|
+
return decodeURIComponent(value);
|
|
12
|
+
} catch {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeSlashes(value: string): string {
|
|
18
|
+
return value.replace(/\\/g, "/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isPathChar(ch: string): boolean {
|
|
22
|
+
return /[A-Za-z0-9._~+%@/\\:-]/.test(ch);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasReferenceBoundaryAfter(text: string, index: number): boolean {
|
|
26
|
+
if (index >= text.length) return true;
|
|
27
|
+
const ch = text[index];
|
|
28
|
+
if (ch === ":") return /\d/.test(text[index + 1] ?? "");
|
|
29
|
+
return !isPathChar(ch);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function containsExactPathReference(text: string, filePath: string): boolean {
|
|
33
|
+
const target = normalizeSlashes(filePath);
|
|
34
|
+
const targets = target.startsWith("/") ? [target, `file://${target}`] : [target];
|
|
35
|
+
const haystacks = new Set([normalizeSlashes(text), normalizeSlashes(safeDecode(text))]);
|
|
36
|
+
|
|
37
|
+
for (const haystack of haystacks) {
|
|
38
|
+
for (const t of targets) {
|
|
39
|
+
let index = haystack.indexOf(t);
|
|
40
|
+
while (index !== -1) {
|
|
41
|
+
const before = index === 0 ? "" : haystack[index - 1];
|
|
42
|
+
const afterIndex = index + t.length;
|
|
43
|
+
if ((index === 0 || !isPathChar(before)) && hasReferenceBoundaryAfter(haystack, afterIndex)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
index = haystack.indexOf(t, index + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectStrings(value: unknown, out: string[]): void {
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
out.push(value);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!value || typeof value !== "object") return;
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
for (const item of value) collectStrings(item, out);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const item of Object.values(value)) collectStrings(item, out);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isFilePathReferencedByEntries(filePath: string, entries: SessionEntry[]): boolean {
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const strings: string[] = [];
|
|
70
|
+
collectStrings(entry, strings);
|
|
71
|
+
if (strings.some((text) => containsExactPathReference(text, filePath))) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isBashOutputPathReferencedByEntries(filePath: string, entries: SessionEntry[]): boolean {
|
|
77
|
+
return entries.some((entry) => (
|
|
78
|
+
entry.type === "message"
|
|
79
|
+
&& entry.message.role === "bashExecution"
|
|
80
|
+
&& entry.message.fullOutputPath === filePath
|
|
81
|
+
));
|
|
82
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
async function loadSubject() {
|
|
5
|
+
return import("./session-file-references-core.ts");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
test("detects exact external file paths referenced in session entries", async () => {
|
|
9
|
+
const { isFilePathReferencedByEntries } = await loadSubject();
|
|
10
|
+
const entries = [
|
|
11
|
+
{
|
|
12
|
+
type: "message",
|
|
13
|
+
id: "entry-1",
|
|
14
|
+
parentId: null,
|
|
15
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
16
|
+
message: {
|
|
17
|
+
role: "assistant",
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: "See [/home/me/.codex/config.toml:12](/home/me/.codex/config.toml:12)",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
assert.equal(isFilePathReferencedByEntries("/home/me/.codex/config.toml", entries), true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("does not authorize sibling files by prefix match", async () => {
|
|
32
|
+
const { isFilePathReferencedByEntries } = await loadSubject();
|
|
33
|
+
const entries = [
|
|
34
|
+
{
|
|
35
|
+
type: "message",
|
|
36
|
+
id: "entry-1",
|
|
37
|
+
parentId: null,
|
|
38
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
39
|
+
message: {
|
|
40
|
+
role: "assistant",
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: "See /home/me/.codex/config.toml.bak",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
assert.equal(isFilePathReferencedByEntries("/home/me/.codex/config.toml", entries), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("authorizes full output only from a bash execution message", async () => {
|
|
55
|
+
const { isBashOutputPathReferencedByEntries } = await loadSubject();
|
|
56
|
+
const outputPath = "/tmp/pi-bash-ab12.log";
|
|
57
|
+
const bashEntry = {
|
|
58
|
+
type: "message",
|
|
59
|
+
id: "entry-1",
|
|
60
|
+
parentId: null,
|
|
61
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
62
|
+
message: {
|
|
63
|
+
role: "bashExecution",
|
|
64
|
+
command: "printf test",
|
|
65
|
+
output: "test",
|
|
66
|
+
fullOutputPath: outputPath,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const assistantEntry = {
|
|
70
|
+
type: "message",
|
|
71
|
+
id: "entry-2",
|
|
72
|
+
parentId: "entry-1",
|
|
73
|
+
timestamp: "2026-01-01T00:00:01.000Z",
|
|
74
|
+
message: {
|
|
75
|
+
role: "assistant",
|
|
76
|
+
content: [{ type: "text", text: `mentioned ${outputPath}` }],
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
assert.equal(isBashOutputPathReferencedByEntries(outputPath, [bashEntry]), true);
|
|
81
|
+
assert.equal(isBashOutputPathReferencedByEntries(outputPath, [assistantEntry]), false);
|
|
82
|
+
assert.equal(isBashOutputPathReferencedByEntries("/tmp/pi-bash-other.log", [bashEntry]), false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("validates session ids before resolving session paths", async () => {
|
|
86
|
+
const { isValidSessionId } = await loadSubject();
|
|
87
|
+
|
|
88
|
+
assert.equal(isValidSessionId("not-a-session-id"), false);
|
|
89
|
+
assert.equal(isValidSessionId("../../sessions/foo"), false);
|
|
90
|
+
assert.equal(isValidSessionId("550e8400-e29b-41d4-a716-446655440000"), true);
|
|
91
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getSessionEntries, resolveSessionPath } from "./session-reader";
|
|
2
|
+
export { isFilePathReferencedByEntries } from "./session-file-references-core";
|
|
3
|
+
import {
|
|
4
|
+
isBashOutputPathReferencedByEntries,
|
|
5
|
+
isFilePathReferencedByEntries,
|
|
6
|
+
isValidSessionId,
|
|
7
|
+
} from "./session-file-references-core";
|
|
8
|
+
|
|
9
|
+
export async function isFilePathReferencedBySession(filePath: string, sessionId: string | null): Promise<boolean> {
|
|
10
|
+
if (!isValidSessionId(sessionId)) return false;
|
|
11
|
+
try {
|
|
12
|
+
const sessionPath = await resolveSessionPath(sessionId);
|
|
13
|
+
if (!sessionPath) return false;
|
|
14
|
+
return isFilePathReferencedByEntries(filePath, getSessionEntries(sessionPath));
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function isBashOutputPathReferencedBySession(filePath: string, sessionId: string | null): Promise<boolean> {
|
|
21
|
+
if (!isValidSessionId(sessionId)) return false;
|
|
22
|
+
try {
|
|
23
|
+
const sessionPath = await resolveSessionPath(sessionId);
|
|
24
|
+
if (!sessionPath) return false;
|
|
25
|
+
return isBashOutputPathReferencedByEntries(filePath, getSessionEntries(sessionPath));
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
async function loadSubject() {
|
|
5
|
+
return import("./session-path.ts");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
test("normalizes Windows separators and casing for session identity", async () => {
|
|
9
|
+
const { sessionPathKey } = await loadSubject();
|
|
10
|
+
|
|
11
|
+
assert.equal(
|
|
12
|
+
sessionPathKey("C:\\Users\\Alex\\.pi\\sessions\\Parent.jsonl", "win32"),
|
|
13
|
+
sessionPathKey("c:/Users/Alex/.pi/sessions/parent.jsonl", "win32"),
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("preserves case when session paths are case-sensitive", async () => {
|
|
18
|
+
const { sessionPathKey } = await loadSubject();
|
|
19
|
+
|
|
20
|
+
assert.notEqual(
|
|
21
|
+
sessionPathKey("/var/lib/pi/Parent.jsonl", "linux"),
|
|
22
|
+
sessionPathKey("/var/lib/pi/parent.jsonl", "linux"),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function sessionPathKey(
|
|
4
|
+
filePath: string,
|
|
5
|
+
platform: NodeJS.Platform = process.platform,
|
|
6
|
+
): string {
|
|
7
|
+
const normalized = platform === "win32"
|
|
8
|
+
? path.win32.normalize(filePath)
|
|
9
|
+
: path.posix.normalize(filePath);
|
|
10
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
11
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Pagination at the data boundary: a linear session (no branching) degrades into
|
|
2
|
+
// a single chain whose depth equals its entry count. The old full-forest read
|
|
3
|
+
// forced O(n) work and was the trigger for #509 (Maximum call stack size
|
|
4
|
+
// exceeded) and #555 (full-history transfer). Slicing bounds both to O(tail).
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
|
|
9
|
+
const jiti = createJiti(import.meta.url);
|
|
10
|
+
const { sliceActiveBranch, buildSessionContext, hasOlderHistory } = await jiti.import("./session-reader.ts");
|
|
11
|
+
|
|
12
|
+
// Build a linear chain of n entries: e0 -> e1 -> ... -> e(n-1).
|
|
13
|
+
function linearChain(n) {
|
|
14
|
+
const entries = [];
|
|
15
|
+
for (let i = 0; i < n; i++) {
|
|
16
|
+
entries.push({
|
|
17
|
+
id: `e${i}`,
|
|
18
|
+
parentId: i === 0 ? null : `e${i - 1}`,
|
|
19
|
+
type: "message",
|
|
20
|
+
timestamp: new Date(1000 + i * 1000).toISOString(),
|
|
21
|
+
message: { role: i % 2 === 0 ? "user" : "assistant", content: `msg ${i}` },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return entries;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("sliceActiveBranch returns the most-recent `tail` ancestors, in time order", () => {
|
|
28
|
+
const entries = linearChain(100);
|
|
29
|
+
const sliced = sliceActiveBranch(entries, "e99", 50);
|
|
30
|
+
assert.equal(sliced.length, 50);
|
|
31
|
+
assert.equal(sliced[0].id, "e50");
|
|
32
|
+
assert.equal(sliced[sliced.length - 1].id, "e99");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("sliceActiveBranch walks from leaf back toward root, not forward", () => {
|
|
36
|
+
const entries = linearChain(10);
|
|
37
|
+
const sliced = sliceActiveBranch(entries, "e5", 3);
|
|
38
|
+
assert.deepEqual(sliced.map((e) => e.id), ["e3", "e4", "e5"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("sliceActiveBranch defaults to the last entry when leafId is null", () => {
|
|
42
|
+
const entries = linearChain(7);
|
|
43
|
+
const sliced = sliceActiveBranch(entries, null, 3);
|
|
44
|
+
assert.deepEqual(sliced.map((e) => e.id), ["e4", "e5", "e6"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("deep linear chain (5000 entries) slices without overflowing the stack", () => {
|
|
48
|
+
const entries = linearChain(5000);
|
|
49
|
+
// The recursion that #509 hit lived in any path-walk over the full chain.
|
|
50
|
+
// An iterative slice over 5000 entries must not throw Maximum call stack size.
|
|
51
|
+
const sliced = sliceActiveBranch(entries, "e4999", 50);
|
|
52
|
+
assert.equal(sliced.length, 50);
|
|
53
|
+
assert.equal(sliced[sliced.length - 1].id, "e4999");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("buildSessionContext with tail returns only the tail window", () => {
|
|
57
|
+
const entries = linearChain(300);
|
|
58
|
+
const ctx = buildSessionContext(entries, "e299", { tail: 50 });
|
|
59
|
+
assert.equal(ctx.messages.length, 50);
|
|
60
|
+
assert.equal(ctx.entryIds.length, 50);
|
|
61
|
+
assert.equal(ctx.entryIds[0], "e250");
|
|
62
|
+
assert.equal(ctx.entryIds[ctx.entryIds.length - 1], "e299");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("buildSessionContext without tail still returns the full chain", () => {
|
|
66
|
+
const entries = linearChain(20);
|
|
67
|
+
const ctx = buildSessionContext(entries, "e19");
|
|
68
|
+
assert.equal(ctx.messages.length, 20);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("buildSessionContext excludeLeaf pages upward without duplicating `before`", () => {
|
|
72
|
+
// User path: client has [e48..e52], requests the page before e48 (older).
|
|
73
|
+
// excludeLeaf must start from e47's parent so e48 is NOT re-fetched.
|
|
74
|
+
const entries = linearChain(100);
|
|
75
|
+
const page1 = buildSessionContext(entries, "e52", { tail: 5 }).entryIds;
|
|
76
|
+
assert.deepEqual(page1, ["e48", "e49", "e50", "e51", "e52"]);
|
|
77
|
+
const oldest = page1[0]; // e48
|
|
78
|
+
const page2 = buildSessionContext(entries, oldest, { tail: 5, excludeLeaf: true }).entryIds;
|
|
79
|
+
assert.equal(page2[page2.length - 1], "e47");
|
|
80
|
+
assert.ok(!page2.includes(oldest), "page2 must not duplicate the `before` boundary");
|
|
81
|
+
// Adjacent pages share no id -> prepending never double-renders.
|
|
82
|
+
assert.ok(page1.every((id) => !page2.includes(id)));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("buildSessionContext accepts a large tail and returns the whole chain", () => {
|
|
86
|
+
const entries = linearChain(5000);
|
|
87
|
+
const ctx = buildSessionContext(entries, "e4999", { tail: 5000 });
|
|
88
|
+
assert.equal(ctx.messages.length, 5000);
|
|
89
|
+
// NOTE: the 1000 cap is enforced at the route layer (Math.min(rawTail, 1000)),
|
|
90
|
+
// see app/api/sessions/[id]/{route,context/route}.test.mjs.
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("real sessions may store assistant content as a string (deferThinking guard)", () => {
|
|
94
|
+
// Regression for the long-session 500: entryToUiMessage calls content.map in
|
|
95
|
+
// the deferThinking branch, but real assistant content can be a plain string.
|
|
96
|
+
const entries = [
|
|
97
|
+
{ id: "u1", parentId: null, type: "message", timestamp: new Date(1).toISOString(),
|
|
98
|
+
message: { role: "user", content: "hi" } },
|
|
99
|
+
{ id: "a1", parentId: "u1", type: "message", timestamp: new Date(2).toISOString(),
|
|
100
|
+
message: { role: "assistant", content: "a string reply, not a block array" } },
|
|
101
|
+
];
|
|
102
|
+
const ctx = buildSessionContext(entries, "a1", { deferThinking: true, tail: 50 });
|
|
103
|
+
assert.equal(ctx.messages.length, 2);
|
|
104
|
+
assert.equal(ctx.messages[1].content, "a string reply, not a block array");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("hasOlderHistory is true only when the active chain exceeds the tail cap", () => {
|
|
108
|
+
assert.equal(hasOlderHistory(linearChain(100), "e99", 50), true);
|
|
109
|
+
assert.equal(hasOlderHistory(linearChain(100), "e99", 5000), false);
|
|
110
|
+
assert.equal(hasOlderHistory(linearChain(30), "e29", 50), false);
|
|
111
|
+
// excludeLeaf pages upward: with 100 entries and a 50-tail page starting at
|
|
112
|
+
// e60, there is still older history; at e0 there is not.
|
|
113
|
+
assert.equal(hasOlderHistory(linearChain(100), "e59", 50, true), true);
|
|
114
|
+
assert.equal(hasOlderHistory(linearChain(100), "e0", 50, true), false);
|
|
115
|
+
});
|