@getpaseo/server 0.1.91-beta.2 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/scripts/supervisor.js +21 -0
  2. package/dist/server/server/agent/agent-manager.d.ts +13 -5
  3. package/dist/server/server/agent/agent-manager.js +110 -74
  4. package/dist/server/server/agent/agent-projections.d.ts +4 -2
  5. package/dist/server/server/agent/agent-projections.js +8 -28
  6. package/dist/server/server/agent/agent-sdk-types.d.ts +30 -10
  7. package/dist/server/server/agent/import-sessions.d.ts +3 -2
  8. package/dist/server/server/agent/import-sessions.js +23 -55
  9. package/dist/server/server/agent/prompt-attachments.js +8 -0
  10. package/dist/server/server/agent/provider-registry.d.ts +0 -1
  11. package/dist/server/server/agent/provider-registry.js +55 -16
  12. package/dist/server/server/agent/provider-session-import.d.ts +10 -0
  13. package/dist/server/server/agent/provider-session-import.js +49 -0
  14. package/dist/server/server/agent/providers/acp-agent.d.ts +12 -2
  15. package/dist/server/server/agent/providers/acp-agent.js +78 -36
  16. package/dist/server/server/agent/providers/claude/agent.d.ts +3 -2
  17. package/dist/server/server/agent/providers/claude/agent.js +28 -24
  18. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +3 -2
  19. package/dist/server/server/agent/providers/codex-app-server-agent.js +29 -26
  20. package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +1 -0
  21. package/dist/server/server/agent/providers/cursor-acp-agent.js +1 -0
  22. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +9 -0
  23. package/dist/server/server/agent/providers/generic-acp-agent.js +18 -1
  24. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +3 -2
  25. package/dist/server/server/agent/providers/mock-load-test-agent.js +11 -1
  26. package/dist/server/server/agent/providers/mock-slow-provider.d.ts +1 -2
  27. package/dist/server/server/agent/providers/mock-slow-provider.js +0 -3
  28. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts +3 -0
  29. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js +12 -0
  30. package/dist/server/server/agent/providers/opencode-agent.d.ts +18 -3
  31. package/dist/server/server/agent/providers/opencode-agent.js +135 -36
  32. package/dist/server/server/agent/providers/pi/agent.d.ts +25 -2
  33. package/dist/server/server/agent/providers/pi/agent.js +243 -14
  34. package/dist/server/server/agent/providers/pi/cli-runtime.js +9 -0
  35. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +9 -0
  36. package/dist/server/server/agent/providers/pi/runtime.d.ts +2 -0
  37. package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +11 -0
  38. package/dist/server/server/agent/providers/pi/session-descriptor.js +284 -0
  39. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +8 -0
  40. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +22 -0
  41. package/dist/server/server/agent/runtime-mcp-config.d.ts +8 -0
  42. package/dist/server/server/agent/runtime-mcp-config.js +50 -0
  43. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +2 -2
  44. package/dist/server/server/daemon-worker.js +84 -1
  45. package/dist/server/server/file-upload/index.d.ts +27 -0
  46. package/dist/server/server/file-upload/index.js +158 -0
  47. package/dist/server/server/loop-service.d.ts +12 -12
  48. package/dist/server/server/persisted-config.d.ts +8 -0
  49. package/dist/server/server/persisted-config.js +1 -1
  50. package/dist/server/server/persistence-hooks.js +6 -4
  51. package/dist/server/server/session.d.ts +5 -2
  52. package/dist/server/server/session.js +20 -3
  53. package/dist/server/server/speech/providers/local/runtime.js +1 -0
  54. package/dist/server/server/speech/providers/local/worker-client.d.ts +14 -1
  55. package/dist/server/server/speech/providers/local/worker-client.js +169 -7
  56. package/dist/server/server/websocket-server.d.ts +2 -0
  57. package/dist/server/server/websocket-server.js +20 -7
  58. package/dist/server/server/workspace-registry.d.ts +4 -4
  59. package/dist/src/server/persisted-config.js +1 -1
  60. package/package.json +5 -5
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { z } from "zod";
6
+ import { importSessionFromPersistence } from "../../provider-session-import.js";
5
7
  import { runProviderTurn } from "../provider-runner.js";
6
8
  import { checkProviderLaunchAvailable, resolveProviderLaunch, } from "../../provider-launch-config.js";
7
9
  import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
@@ -10,6 +12,7 @@ import { buildBinaryDiagnosticRows, formatDiagnosticStatus, formatProviderDiagno
10
12
  import { getUserMessageText, streamPiHistory, } from "./history-mapper.js";
11
13
  import { PiCliRuntime } from "./cli-runtime.js";
12
14
  import { revertPiConversation } from "./rewind.js";
15
+ import { listPiImportableSessions } from "./session-descriptor.js";
13
16
  import { mapToolDetail, parseToolArgs, parseToolResult, resolveToolCallName, } from "./tool-call-mapper.js";
14
17
  const PI_PROVIDER = "pi";
15
18
  const DEFAULT_PI_THINKING_LEVEL = "medium";
@@ -23,9 +26,35 @@ const QUESTION_RESPONSE_HEADER = "Response";
23
26
  const QUESTION_COMMENT_HEADER = "Comment";
24
27
  const PI_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
25
28
  const COMBINED_ASK_USER_METADATA = "ask_user_select_optional_comment";
29
+ export const PiProviderParamsSchema = z
30
+ .object({
31
+ sessionDir: z.string().min(1).optional(),
32
+ })
33
+ .strict();
34
+ const PI_HANDLED_BUILTIN_SLASH_COMMANDS = [
35
+ {
36
+ name: "compact",
37
+ description: "Manually compact the session context",
38
+ argumentHint: "[instructions]",
39
+ kind: "command",
40
+ },
41
+ {
42
+ name: "autocompact",
43
+ description: "Toggle automatic context compaction",
44
+ argumentHint: "[on|off|toggle]",
45
+ kind: "command",
46
+ },
47
+ ];
48
+ function mapPiCommandKind(source) {
49
+ if (source === "skill") {
50
+ return "skill";
51
+ }
52
+ return "command";
53
+ }
26
54
  const PI_CAPABILITIES = {
27
55
  supportsStreaming: true,
28
56
  supportsSessionPersistence: true,
57
+ supportsSessionListing: true,
29
58
  supportsDynamicModes: true,
30
59
  supportsMcpServers: false,
31
60
  supportsReasoningStream: true,
@@ -43,7 +72,12 @@ const PI_THINKING_OPTIONS = [
43
72
  { id: "xhigh", label: "XHigh", description: "Maximum reasoning" },
44
73
  ];
45
74
  function normalizePiModelLabel(label) {
46
- return label.trim().replace(/[_\s]+/g, " ");
75
+ const normalizedLabel = label.trim().replace(/[_\s]+/g, " ");
76
+ const vendorSeparatorIndex = normalizedLabel.indexOf(": ");
77
+ if (vendorSeparatorIndex === -1) {
78
+ return normalizedLabel;
79
+ }
80
+ return normalizedLabel.slice(vendorSeparatorIndex + 2).trim();
47
81
  }
48
82
  export function transformPiModels(models) {
49
83
  return models.map((model) => {
@@ -76,6 +110,19 @@ function normalizePiThinkingOption(value) {
76
110
  }
77
111
  return isPiThinkingLevel(value) ? value : null;
78
112
  }
113
+ function parseAutoCompactMode(value) {
114
+ const mode = (value ?? "toggle").trim().toLowerCase();
115
+ if (mode === "on" || mode === "true" || mode === "enable" || mode === "enabled") {
116
+ return true;
117
+ }
118
+ if (mode === "off" || mode === "false" || mode === "disable" || mode === "disabled") {
119
+ return false;
120
+ }
121
+ if (mode === "toggle") {
122
+ return "toggle";
123
+ }
124
+ return "unknown";
125
+ }
79
126
  function mapThinkingOption(option) {
80
127
  const mappedOption = {
81
128
  id: option.id,
@@ -654,6 +701,9 @@ export class PiRpcAgentSession {
654
701
  this.seenUserEntryIds = new Set();
655
702
  this.pendingUserMessages = [];
656
703
  this.pendingExtensionResults = new Map();
704
+ this.outOfBandCompactionEmit = null;
705
+ this.outOfBandCompactionStarted = false;
706
+ this.outOfBandCompactionCompleted = false;
657
707
  this.closed = false;
658
708
  this.runtimeSession = options.runtimeSession;
659
709
  this.config = options.config;
@@ -821,11 +871,42 @@ export class PiRpcAgentSession {
821
871
  }
822
872
  async listCommands() {
823
873
  const commands = await this.runtimeSession.getCommands();
824
- return commands.map((command) => ({
825
- name: command.name,
826
- description: command.description ?? command.source,
827
- argumentHint: "",
828
- }));
874
+ const mappedCommands = new Map(PI_HANDLED_BUILTIN_SLASH_COMMANDS.map((command) => [command.name, { ...command }]));
875
+ for (const command of commands) {
876
+ const knownCommand = mappedCommands.get(command.name);
877
+ mappedCommands.set(command.name, {
878
+ name: command.name,
879
+ description: command.description ?? command.source,
880
+ argumentHint: knownCommand?.argumentHint ?? "",
881
+ kind: mapPiCommandKind(command.source),
882
+ });
883
+ }
884
+ return [...mappedCommands.values()];
885
+ }
886
+ tryHandleOutOfBand(prompt) {
887
+ if (typeof prompt !== "string") {
888
+ return null;
889
+ }
890
+ const parsed = this.parseSlashCommandInput(prompt);
891
+ if (!parsed) {
892
+ return null;
893
+ }
894
+ const commandName = parsed.commandName.toLowerCase();
895
+ if (commandName === "compact") {
896
+ return {
897
+ run: async ({ emit }) => {
898
+ await this.executeCompactCommand(parsed.args, emit);
899
+ },
900
+ };
901
+ }
902
+ if (commandName === "autocompact") {
903
+ return {
904
+ run: async ({ emit }) => {
905
+ await this.executeAutoCompactCommand(parsed.args, emit);
906
+ },
907
+ };
908
+ }
909
+ return null;
829
910
  }
830
911
  async setModel(modelId) {
831
912
  const parsedReference = parseModelReference(modelId);
@@ -860,6 +941,117 @@ export class PiRpcAgentSession {
860
941
  currentTurnIdForEvent() {
861
942
  return this.activeTurnId ?? undefined;
862
943
  }
944
+ parseSlashCommandInput(text) {
945
+ const trimmed = text.trim();
946
+ if (!trimmed.startsWith("/") || trimmed.length <= 1) {
947
+ return null;
948
+ }
949
+ const withoutPrefix = trimmed.slice(1);
950
+ const firstWhitespaceIdx = withoutPrefix.search(/\s/);
951
+ const commandName = firstWhitespaceIdx === -1 ? withoutPrefix : withoutPrefix.slice(0, firstWhitespaceIdx);
952
+ if (!commandName || commandName.includes("/")) {
953
+ return null;
954
+ }
955
+ const rawArgs = firstWhitespaceIdx === -1 ? "" : withoutPrefix.slice(firstWhitespaceIdx + 1).trim();
956
+ return rawArgs.length > 0 ? { commandName, args: rawArgs } : { commandName };
957
+ }
958
+ async executeCompactCommand(customInstructions, emit) {
959
+ if (this.outOfBandCompactionEmit) {
960
+ throw new Error("A Pi compact command is already running");
961
+ }
962
+ this.outOfBandCompactionEmit = emit;
963
+ this.outOfBandCompactionStarted = false;
964
+ this.outOfBandCompactionCompleted = false;
965
+ try {
966
+ await this.runtimeSession.compact(customInstructions);
967
+ }
968
+ catch (error) {
969
+ const message = error instanceof Error ? error.message : String(error);
970
+ if (this.outOfBandCompactionEmit === emit &&
971
+ this.outOfBandCompactionStarted &&
972
+ !this.outOfBandCompactionCompleted) {
973
+ this.emitCompactionTimeline({
974
+ turnId: undefined,
975
+ item: {
976
+ type: "compaction",
977
+ status: "completed",
978
+ trigger: "manual",
979
+ },
980
+ });
981
+ }
982
+ emit({
983
+ type: "timeline",
984
+ provider: PI_PROVIDER,
985
+ item: {
986
+ type: "assistant_message",
987
+ text: `[Error] Failed to compact context: ${message}`,
988
+ },
989
+ });
990
+ }
991
+ finally {
992
+ if (this.outOfBandCompactionEmit === emit && !this.outOfBandCompactionStarted) {
993
+ this.outOfBandCompactionEmit = null;
994
+ this.outOfBandCompactionStarted = false;
995
+ this.outOfBandCompactionCompleted = false;
996
+ }
997
+ }
998
+ }
999
+ async executeAutoCompactCommand(mode, emit) {
1000
+ let enabled = parseAutoCompactMode(mode);
1001
+ if (enabled === "unknown") {
1002
+ emit({
1003
+ type: "timeline",
1004
+ provider: PI_PROVIDER,
1005
+ item: {
1006
+ type: "assistant_message",
1007
+ text: "[Error] Usage: /autocompact [on|off|toggle]",
1008
+ },
1009
+ });
1010
+ return;
1011
+ }
1012
+ if (enabled === "toggle") {
1013
+ const state = await this.runtimeSession.getState();
1014
+ if (typeof state.autoCompactionEnabled !== "boolean") {
1015
+ emit({
1016
+ type: "timeline",
1017
+ provider: PI_PROVIDER,
1018
+ item: {
1019
+ type: "assistant_message",
1020
+ text: "[Error] Auto-compaction state is unavailable. Use /autocompact on or /autocompact off.",
1021
+ },
1022
+ });
1023
+ return;
1024
+ }
1025
+ enabled = !state.autoCompactionEnabled;
1026
+ }
1027
+ try {
1028
+ await this.runtimeSession.setAutoCompaction(enabled);
1029
+ }
1030
+ catch (error) {
1031
+ const message = error instanceof Error ? error.message : String(error);
1032
+ emit({
1033
+ type: "timeline",
1034
+ provider: PI_PROVIDER,
1035
+ item: {
1036
+ type: "assistant_message",
1037
+ text: `[Error] Failed to set auto-compaction: ${message}`,
1038
+ },
1039
+ });
1040
+ return;
1041
+ }
1042
+ this.state = {
1043
+ ...this.state,
1044
+ autoCompactionEnabled: enabled,
1045
+ };
1046
+ emit({
1047
+ type: "timeline",
1048
+ provider: PI_PROVIDER,
1049
+ item: {
1050
+ type: "assistant_message",
1051
+ text: `Auto-compaction ${enabled ? "enabled" : "disabled"}.`,
1052
+ },
1053
+ });
1054
+ }
863
1055
  async requestEntryCapture(reason) {
864
1056
  const requestId = randomUUID();
865
1057
  const resultPromise = this.waitForExtensionResult(requestId);
@@ -1090,9 +1282,7 @@ export class PiRpcAgentSession {
1090
1282
  return;
1091
1283
  }
1092
1284
  case "compaction_start":
1093
- this.emit({
1094
- type: "timeline",
1095
- provider: PI_PROVIDER,
1285
+ this.emitCompactionTimeline({
1096
1286
  turnId,
1097
1287
  item: {
1098
1288
  type: "compaction",
@@ -1102,13 +1292,12 @@ export class PiRpcAgentSession {
1102
1292
  });
1103
1293
  return;
1104
1294
  case "compaction_end":
1105
- this.emit({
1106
- type: "timeline",
1107
- provider: PI_PROVIDER,
1295
+ this.emitCompactionTimeline({
1108
1296
  turnId,
1109
1297
  item: {
1110
1298
  type: "compaction",
1111
1299
  status: "completed",
1300
+ trigger: event.reason === "manual" ? "manual" : "auto",
1112
1301
  },
1113
1302
  });
1114
1303
  return;
@@ -1119,6 +1308,33 @@ export class PiRpcAgentSession {
1119
1308
  return;
1120
1309
  }
1121
1310
  }
1311
+ emitCompactionTimeline(input) {
1312
+ const emitOutOfBand = this.outOfBandCompactionEmit;
1313
+ if (emitOutOfBand && input.item.type === "compaction") {
1314
+ if (input.item.status === "loading") {
1315
+ this.outOfBandCompactionStarted = true;
1316
+ }
1317
+ if (input.item.status === "completed") {
1318
+ this.outOfBandCompactionCompleted = true;
1319
+ }
1320
+ }
1321
+ const event = {
1322
+ type: "timeline",
1323
+ provider: PI_PROVIDER,
1324
+ ...(emitOutOfBand ? {} : { turnId: input.turnId }),
1325
+ item: input.item,
1326
+ };
1327
+ if (emitOutOfBand) {
1328
+ emitOutOfBand(event);
1329
+ if (input.item.type === "compaction" && input.item.status === "completed") {
1330
+ this.outOfBandCompactionEmit = null;
1331
+ this.outOfBandCompactionStarted = false;
1332
+ this.outOfBandCompactionCompleted = false;
1333
+ }
1334
+ return;
1335
+ }
1336
+ this.emit(event);
1337
+ }
1122
1338
  handleMessageUpdate(event, turnId) {
1123
1339
  if (event.message.role !== "assistant") {
1124
1340
  return;
@@ -1240,6 +1456,7 @@ export class PiRpcAgentClient {
1240
1456
  this.capabilities = PI_CAPABILITIES;
1241
1457
  this.logger = options.logger;
1242
1458
  this.runtimeSettings = options.runtimeSettings;
1459
+ this.providerParams = PiProviderParamsSchema.parse(options.providerParams ?? {});
1243
1460
  this.runtime = options.runtime ?? createRuntime(options.logger, options.runtimeSettings);
1244
1461
  }
1245
1462
  async createSession(config, launchContext) {
@@ -1332,8 +1549,20 @@ export class PiRpcAgentClient {
1332
1549
  async listModes(_options) {
1333
1550
  return [];
1334
1551
  }
1335
- async listPersistedAgents(_options) {
1336
- return [];
1552
+ async listImportableSessions(options) {
1553
+ return await listPiImportableSessions({
1554
+ ...options,
1555
+ sessionDir: this.providerParams.sessionDir,
1556
+ runtimeSettings: this.runtimeSettings,
1557
+ });
1558
+ }
1559
+ async importSession(input, context) {
1560
+ return importSessionFromPersistence({
1561
+ provider: PI_PROVIDER,
1562
+ request: input,
1563
+ context,
1564
+ resumeSession: this.resumeSession.bind(this),
1565
+ });
1337
1566
  }
1338
1567
  async isAvailable() {
1339
1568
  const launch = await this.resolvePiLaunch();
@@ -76,6 +76,15 @@ class PiCliRuntimeSession {
76
76
  async prompt(message, images) {
77
77
  await this.request({ type: "prompt", message, ...(images?.length ? { images } : {}) });
78
78
  }
79
+ async compact(customInstructions) {
80
+ await this.request({
81
+ type: "compact",
82
+ ...(customInstructions ? { customInstructions } : {}),
83
+ });
84
+ }
85
+ async setAutoCompaction(enabled) {
86
+ await this.request({ type: "set_auto_compaction", enabled });
87
+ }
79
88
  async abort() {
80
89
  await this.request({ type: "abort" });
81
90
  }
@@ -66,6 +66,7 @@ export interface PiSessionState {
66
66
  thinkingLevel: PiThinkingLevel;
67
67
  isStreaming: boolean;
68
68
  isCompacting: boolean;
69
+ autoCompactionEnabled?: boolean;
69
70
  sessionFile?: string;
70
71
  sessionId: string;
71
72
  sessionName?: string;
@@ -98,6 +99,14 @@ export type PiRpcCommand = {
98
99
  type: "prompt";
99
100
  message: string;
100
101
  images?: PiImageContent[];
102
+ } | {
103
+ id?: string;
104
+ type: "compact";
105
+ customInstructions?: string;
106
+ } | {
107
+ id?: string;
108
+ type: "set_auto_compaction";
109
+ enabled: boolean;
101
110
  } | {
102
111
  id?: string;
103
112
  type: "abort";
@@ -28,6 +28,8 @@ export interface PiRuntimeSession {
28
28
  data: string;
29
29
  mimeType: string;
30
30
  }>): Promise<void>;
31
+ compact(customInstructions?: string): Promise<void>;
32
+ setAutoCompaction(enabled: boolean): Promise<void>;
31
33
  abort(): Promise<void>;
32
34
  getState(): Promise<PiSessionState>;
33
35
  getMessages(): Promise<PiAgentMessage[]>;
@@ -0,0 +1,11 @@
1
+ import type { ImportableProviderSession, ListImportableSessionsOptions } from "../../agent-sdk-types.js";
2
+ import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
3
+ interface PiSessionDescriptorOptions extends ListImportableSessionsOptions {
4
+ sessionDir?: string;
5
+ runtimeSettings?: ProviderRuntimeSettings;
6
+ env?: NodeJS.ProcessEnv;
7
+ homeDir?: string;
8
+ }
9
+ export declare function listPiImportableSessions(options?: PiSessionDescriptorOptions): Promise<ImportableProviderSession[]>;
10
+ export {};
11
+ //# sourceMappingURL=session-descriptor.d.ts.map
@@ -0,0 +1,284 @@
1
+ import { open, readdir, readFile, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { createRealpathAwarePathMatcher } from "../../../../utils/path.js";
5
+ const PI_CONFIG_DIR_NAME = ".pi";
6
+ const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
7
+ const PI_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
8
+ const HEAD_BYTES = 64 * 1024;
9
+ const TAIL_BYTES = 256 * 1024;
10
+ const FULL_SCAN_LINE_LIMIT = 2000;
11
+ export async function listPiImportableSessions(options = {}) {
12
+ const sessionsDir = await resolvePiSessionsDir(options);
13
+ const files = await walkJsonlFiles(sessionsDir);
14
+ const matchesCwd = options.cwd ? createRealpathAwarePathMatcher(options.cwd) : null;
15
+ const limit = options.limit ?? 20;
16
+ const sessions = [];
17
+ for (const file of files) {
18
+ const session = await readPiImportableSession(file);
19
+ if (!session)
20
+ continue;
21
+ if (matchesCwd && !matchesCwd(session.cwd))
22
+ continue;
23
+ sessions.push(session);
24
+ }
25
+ return sessions
26
+ .sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
27
+ .slice(0, limit);
28
+ }
29
+ async function resolvePiSessionsDir(options) {
30
+ const env = options.env ?? process.env;
31
+ const homeDir = options.homeDir ?? homedir();
32
+ const baseDir = options.cwd ?? process.cwd();
33
+ if (options.sessionDir?.trim()) {
34
+ return resolveConfigPath(options.sessionDir, { baseDir, homeDir });
35
+ }
36
+ const agentDir = resolvePiAgentDir({ runtimeSettings: options.runtimeSettings, env, homeDir });
37
+ const envSessionDir = options.runtimeSettings?.env?.[PI_SESSION_DIR_ENV] ?? env[PI_SESSION_DIR_ENV];
38
+ if (envSessionDir?.trim()) {
39
+ return resolveConfigPath(envSessionDir, { baseDir, homeDir });
40
+ }
41
+ const settingsSessionDir = await readConfiguredSessionDir({
42
+ agentDir,
43
+ cwd: options.cwd,
44
+ });
45
+ if (settingsSessionDir?.trim()) {
46
+ return resolveConfigPath(settingsSessionDir, { baseDir, homeDir });
47
+ }
48
+ return path.join(agentDir, "sessions");
49
+ }
50
+ function resolvePiAgentDir(input) {
51
+ const configured = input.runtimeSettings?.env?.[PI_AGENT_DIR_ENV] ?? input.env[PI_AGENT_DIR_ENV];
52
+ if (configured?.trim()) {
53
+ return resolveConfigPath(configured, { baseDir: process.cwd(), homeDir: input.homeDir });
54
+ }
55
+ return path.join(input.homeDir, PI_CONFIG_DIR_NAME, "agent");
56
+ }
57
+ async function readConfiguredSessionDir(input) {
58
+ const values = await Promise.all([
59
+ readSessionDirFromSettings(path.join(input.agentDir, "settings.json")),
60
+ input.cwd
61
+ ? readSessionDirFromSettings(path.join(input.cwd, PI_CONFIG_DIR_NAME, "settings.json"))
62
+ : null,
63
+ ]);
64
+ return values[1] ?? values[0] ?? null;
65
+ }
66
+ async function readSessionDirFromSettings(settingsPath) {
67
+ try {
68
+ const parsed = JSON.parse(await readFile(settingsPath, "utf8"));
69
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
70
+ return null;
71
+ }
72
+ const sessionDir = Reflect.get(parsed, "sessionDir");
73
+ return typeof sessionDir === "string" && sessionDir.trim() ? sessionDir : null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ function resolveConfigPath(value, options) {
80
+ if (value === "~") {
81
+ return options.homeDir;
82
+ }
83
+ if (value.startsWith("~/")) {
84
+ return path.join(options.homeDir, value.slice(2));
85
+ }
86
+ return path.isAbsolute(value) ? value : path.resolve(options.baseDir, value);
87
+ }
88
+ async function walkJsonlFiles(root) {
89
+ let entries;
90
+ try {
91
+ entries = await readdir(root, { withFileTypes: true });
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ const files = await Promise.all(entries.map(async (entry) => {
97
+ const entryPath = path.join(root, entry.name);
98
+ if (entry.isDirectory()) {
99
+ return await walkJsonlFiles(entryPath);
100
+ }
101
+ return entry.isFile() && entry.name.endsWith(".jsonl") ? [entryPath] : [];
102
+ }));
103
+ return files.flat();
104
+ }
105
+ async function readPiImportableSession(filePath) {
106
+ const firstLine = await readFirstLine(filePath);
107
+ if (!firstLine)
108
+ return null;
109
+ const header = parseSessionHeader(firstLine);
110
+ if (!header)
111
+ return null;
112
+ const tail = await readTail(filePath).catch(() => "");
113
+ const tailInfo = parseSessionTail(tail);
114
+ const headInfo = await scanSessionHead(filePath);
115
+ const title = tailInfo.title ?? headInfo.title ?? headInfo.firstUserMessage;
116
+ const lastActivityAt = tailInfo.lastActivityAt ?? (await readFileMtime(filePath)) ?? header.createdAt ?? new Date(0);
117
+ return {
118
+ providerHandleId: filePath,
119
+ cwd: header.cwd,
120
+ title,
121
+ firstPromptPreview: normalizePromptPreview(headInfo.firstUserMessage),
122
+ lastPromptPreview: normalizePromptPreview(tailInfo.lastUserMessage ?? headInfo.firstUserMessage),
123
+ lastActivityAt,
124
+ };
125
+ }
126
+ async function readFirstLine(filePath) {
127
+ const handle = await open(filePath, "r").catch(() => null);
128
+ if (!handle)
129
+ return null;
130
+ try {
131
+ const buffer = Buffer.alloc(HEAD_BYTES);
132
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
133
+ if (bytesRead <= 0)
134
+ return null;
135
+ const chunk = buffer.subarray(0, bytesRead).toString("utf8");
136
+ const newlineIndex = chunk.indexOf("\n");
137
+ return (newlineIndex === -1 ? chunk : chunk.slice(0, newlineIndex)).trim();
138
+ }
139
+ finally {
140
+ await handle.close().catch(() => undefined);
141
+ }
142
+ }
143
+ async function readTail(filePath) {
144
+ const fileStats = await stat(filePath);
145
+ const start = Math.max(0, fileStats.size - TAIL_BYTES);
146
+ const length = fileStats.size - start;
147
+ const handle = await open(filePath, "r");
148
+ try {
149
+ const buffer = Buffer.alloc(length);
150
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
151
+ return buffer.subarray(0, bytesRead).toString("utf8");
152
+ }
153
+ finally {
154
+ await handle.close().catch(() => undefined);
155
+ }
156
+ }
157
+ async function readFileMtime(filePath) {
158
+ try {
159
+ return (await stat(filePath)).mtime;
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ function parseSessionHeader(firstLine) {
166
+ const entry = parseJsonRecord(firstLine);
167
+ if (!entry || entry.type !== "session")
168
+ return null;
169
+ const sessionId = typeof entry.id === "string" ? entry.id : null;
170
+ const cwd = typeof entry.cwd === "string" ? entry.cwd : null;
171
+ if (!sessionId || !cwd)
172
+ return null;
173
+ const createdAt = parseDate(entry.timestamp);
174
+ return { sessionId, cwd, createdAt };
175
+ }
176
+ function parseSessionTail(tail) {
177
+ const lines = tail.split(/\r?\n/u);
178
+ let title = null;
179
+ let lastActivityAt = null;
180
+ let fallbackTimestamp = null;
181
+ let lastUserMessage = null;
182
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
183
+ const entry = parseJsonRecord(lines[index].trim());
184
+ if (!entry)
185
+ continue;
186
+ if (!title && entry.type === "session_info") {
187
+ title = readNonEmptyString(entry.name);
188
+ }
189
+ const entryTimestamp = parseDate(entry.timestamp);
190
+ if (!fallbackTimestamp && entryTimestamp) {
191
+ fallbackTimestamp = entryTimestamp;
192
+ }
193
+ if (entry.type !== "message") {
194
+ continue;
195
+ }
196
+ if (!lastActivityAt && entryTimestamp) {
197
+ lastActivityAt = entryTimestamp;
198
+ }
199
+ if (!lastUserMessage && isRecord(entry.message) && entry.message.role === "user") {
200
+ lastUserMessage = extractMessageText(entry.message.content);
201
+ }
202
+ if (title && lastActivityAt && lastUserMessage) {
203
+ break;
204
+ }
205
+ }
206
+ return { title, lastActivityAt: lastActivityAt ?? fallbackTimestamp, lastUserMessage };
207
+ }
208
+ async function scanSessionHead(filePath) {
209
+ let content;
210
+ try {
211
+ content = await readFile(filePath, "utf8");
212
+ }
213
+ catch {
214
+ return { title: null, firstUserMessage: null };
215
+ }
216
+ let title = null;
217
+ let firstUserMessage = null;
218
+ let lineCount = 0;
219
+ for (const rawLine of content.split(/\r?\n/u)) {
220
+ lineCount += 1;
221
+ const entry = parseJsonRecord(rawLine.trim());
222
+ if (!entry)
223
+ continue;
224
+ if (entry.type === "session_info") {
225
+ title = readNonEmptyString(entry.name) ?? title;
226
+ }
227
+ if (!firstUserMessage && entry.type === "message" && isRecord(entry.message)) {
228
+ if (entry.message.role === "user") {
229
+ firstUserMessage = extractMessageText(entry.message.content);
230
+ }
231
+ }
232
+ if (title && firstUserMessage) {
233
+ break;
234
+ }
235
+ if (lineCount >= FULL_SCAN_LINE_LIMIT && firstUserMessage) {
236
+ break;
237
+ }
238
+ }
239
+ return { title, firstUserMessage };
240
+ }
241
+ function parseJsonRecord(line) {
242
+ if (!line)
243
+ return null;
244
+ try {
245
+ const parsed = JSON.parse(line);
246
+ return isRecord(parsed) ? parsed : null;
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ function isRecord(value) {
253
+ return typeof value === "object" && value !== null && !Array.isArray(value);
254
+ }
255
+ function readNonEmptyString(value) {
256
+ return typeof value === "string" && value.trim() ? value.trim() : null;
257
+ }
258
+ function normalizePromptPreview(text) {
259
+ const normalized = text?.trim().replace(/\s+/g, " ") ?? "";
260
+ if (!normalized)
261
+ return null;
262
+ return normalized.length > 160 ? normalized.slice(0, 160) : normalized;
263
+ }
264
+ function parseDate(value) {
265
+ if (typeof value !== "string" && typeof value !== "number") {
266
+ return null;
267
+ }
268
+ const date = new Date(value);
269
+ return Number.isNaN(date.getTime()) ? null : date;
270
+ }
271
+ function extractMessageText(content) {
272
+ if (typeof content === "string") {
273
+ return content.trim() || null;
274
+ }
275
+ if (!Array.isArray(content)) {
276
+ return null;
277
+ }
278
+ const text = content
279
+ .flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : [])
280
+ .join("\n\n")
281
+ .trim();
282
+ return text || null;
283
+ }
284
+ //# sourceMappingURL=session-descriptor.js.map