pi-sessions 0.6.0 → 0.7.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.
Files changed (43) hide show
  1. package/README.md +22 -11
  2. package/extensions/session-ask/agent.ts +400 -0
  3. package/extensions/session-ask/navigate.ts +880 -0
  4. package/extensions/session-ask.ts +79 -131
  5. package/extensions/session-auto-title/model.ts +3 -11
  6. package/extensions/session-handoff/picker.ts +50 -4
  7. package/extensions/session-handoff/query.ts +74 -56
  8. package/extensions/session-handoff/refs.ts +12 -18
  9. package/extensions/session-handoff.ts +36 -27
  10. package/extensions/session-messaging/broker/process.ts +23 -26
  11. package/extensions/session-messaging/broker/spawn.ts +7 -2
  12. package/extensions/session-messaging/pi/incoming-runtime.ts +2 -18
  13. package/extensions/session-messaging/pi/message-contracts.ts +9 -10
  14. package/extensions/session-messaging/pi/message-view.ts +12 -1
  15. package/extensions/session-messaging/pi/service.ts +117 -62
  16. package/extensions/session-messaging/pi/tools.ts +4 -56
  17. package/extensions/session-search/extract.ts +86 -436
  18. package/extensions/session-search/hooks.ts +17 -25
  19. package/extensions/session-search/normalize.ts +1 -1
  20. package/extensions/session-search/reindex.ts +1 -0
  21. package/extensions/session-search.ts +157 -128
  22. package/extensions/shared/model.ts +18 -0
  23. package/extensions/shared/session-broker/active.ts +26 -0
  24. package/extensions/{session-messaging/pi → shared/session-broker}/client.ts +16 -19
  25. package/extensions/{session-messaging/shared → shared/session-broker}/framing.ts +1 -1
  26. package/extensions/{session-messaging/shared → shared/session-broker}/protocol.ts +5 -12
  27. package/extensions/shared/session-index/access.ts +128 -0
  28. package/extensions/shared/session-index/common.ts +43 -48
  29. package/extensions/shared/session-index/index.ts +1 -0
  30. package/extensions/shared/session-index/lineage.ts +10 -0
  31. package/extensions/shared/session-index/query/ast.ts +40 -0
  32. package/extensions/shared/session-index/query/compiler.ts +146 -0
  33. package/extensions/shared/session-index/query/lexer.ts +140 -0
  34. package/extensions/shared/session-index/query/parser.ts +178 -0
  35. package/extensions/shared/session-index/schema.ts +22 -6
  36. package/extensions/shared/session-index/scoring.ts +80 -0
  37. package/extensions/shared/session-index/search.ts +552 -278
  38. package/extensions/shared/session-index/sqlite.ts +16 -9
  39. package/extensions/shared/session-index/store.ts +12 -21
  40. package/extensions/shared/settings.ts +61 -4
  41. package/extensions/shared/text.ts +50 -0
  42. package/package.json +1 -1
  43. /package/extensions/{session-messaging/shared → shared/session-broker}/socket-path.ts +0 -0
@@ -48,22 +48,29 @@ function loadDatabaseConstructor(): SqliteConstructor {
48
48
 
49
49
  export function openSqlite(
50
50
  dbPath: string,
51
- options: { create: boolean; timeoutMs?: number | undefined },
51
+ options: { create: boolean; readonly?: boolean | undefined; timeoutMs?: number | undefined },
52
52
  ): SqliteDatabase {
53
53
  const Database = loadDatabaseConstructor();
54
+ const readonly = options.readonly ?? false;
54
55
  const db = isBun
55
- ? new Database(dbPath, { create: options.create, readwrite: true })
56
- : new Database(dbPath, { fileMustExist: !options.create });
56
+ ? new Database(dbPath, {
57
+ create: readonly ? false : options.create,
58
+ readonly,
59
+ readwrite: !readonly,
60
+ })
61
+ : new Database(dbPath, { fileMustExist: readonly || !options.create, readonly });
57
62
 
58
- // busy_timeout must be set before any other statement: the journal_mode
59
- // pragma below is the connection's first lock acquisition (and may run WAL
60
- // recovery), and immediate transactions rely on this timeout to queue behind
61
- // concurrent writers instead of failing.
62
63
  db.exec(`PRAGMA busy_timeout = ${options.timeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS}`);
63
- db.exec("PRAGMA journal_mode = WAL");
64
- db.exec("PRAGMA synchronous = NORMAL");
65
64
  db.exec("PRAGMA foreign_keys = ON");
66
65
 
66
+ if (!readonly) {
67
+ // journal_mode is the connection's first write-capable lock acquisition (and
68
+ // may run WAL recovery), and immediate transactions rely on busy_timeout to
69
+ // queue behind concurrent writers instead of failing.
70
+ db.exec("PRAGMA journal_mode = WAL");
71
+ db.exec("PRAGMA synchronous = NORMAL");
72
+ }
73
+
67
74
  return withStatementCache(db);
68
75
  }
69
76
 
@@ -1,7 +1,7 @@
1
+ import path from "node:path";
1
2
  import { type Static, Type } from "typebox";
2
3
  import { parseTypeBoxValue } from "../typebox.ts";
3
4
  import {
4
- compactSessionId,
5
5
  INDEX_SCHEMA_VERSION,
6
6
  NULLABLE_STRING_SCHEMA,
7
7
  parseRepoRoots,
@@ -76,7 +76,7 @@ export function insertSession(
76
76
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
77
77
  `,
78
78
  ).run(...sessionRowBindings(row, indexSource));
79
- insertSessionIdChunk(db, row);
79
+ syncSessionRepoRoots(db, row);
80
80
  }
81
81
 
82
82
  export function upsertSession(
@@ -117,7 +117,7 @@ export function upsertSession(
117
117
  index_source = excluded.index_source
118
118
  `,
119
119
  ).run(...sessionRowBindings(row, indexSource));
120
- syncSessionIdChunk(db, row);
120
+ syncSessionRepoRoots(db, row);
121
121
  }
122
122
 
123
123
  export function getSessionRowByPath(
@@ -186,24 +186,15 @@ function buildSessionRow(row: Static<typeof SESSION_ROW_QUERY_SCHEMA>): SessionR
186
186
  };
187
187
  }
188
188
 
189
- function insertSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
190
- insertTextChunk(db, {
191
- sessionId: row.sessionId,
192
- entryType: "session_info",
193
- ts: row.modifiedAt,
194
- sourceKind: "session_id",
195
- text: buildSessionIdSearchText(row.sessionId),
196
- });
197
- }
198
-
199
- function syncSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
200
- clearSessionChunksBySourceKind(db, row.sessionId, "session_id");
201
- insertSessionIdChunk(db, row);
202
- }
189
+ function syncSessionRepoRoots(db: SessionIndexDatabase, row: SessionRow): void {
190
+ db.prepare(`DELETE FROM session_repo_roots WHERE session_id = ?`).run(row.sessionId);
203
191
 
204
- function buildSessionIdSearchText(sessionId: string): string {
205
- const compact = compactSessionId(sessionId);
206
- return compact === sessionId ? sessionId : `${sessionId} ${compact}`;
192
+ const insert = db.prepare(
193
+ `INSERT INTO session_repo_roots(session_id, repo_root, repo_basename) VALUES (?, ?, ?)`,
194
+ );
195
+ for (const repoRoot of [...new Set(row.repoRoots)]) {
196
+ insert.run(row.sessionId, repoRoot, path.basename(repoRoot));
197
+ }
207
198
  }
208
199
 
209
200
  export function clearSessionChunksBySourceKind(
@@ -231,7 +222,7 @@ export function insertTextChunk(db: SessionIndexDatabase, row: SessionTextChunkR
231
222
  `,
232
223
  ).run(
233
224
  row.sessionId,
234
- row.entryId ?? null,
225
+ row.entryId,
235
226
  row.entryType,
236
227
  row.role ?? null,
237
228
  row.ts,
@@ -1,5 +1,6 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
3
4
  import { SettingsManager } from "@earendil-works/pi-coding-agent";
4
5
  import type { KeyId } from "@earendil-works/pi-tui";
5
6
  import { type Static, Type } from "typebox";
@@ -22,9 +23,17 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
22
23
  Type.Object({
23
24
  refreshTurns: Type.Optional(Type.Integer({ minimum: 1 })),
24
25
  model: Type.Optional(Type.String()),
26
+ thinkingLevel: Type.Optional(Type.String()),
25
27
  prompt: Type.Optional(Type.String()),
26
28
  }),
27
29
  ),
30
+ ask: Type.Optional(
31
+ Type.Object({
32
+ model: Type.Optional(Type.String()),
33
+ thinkingLevel: Type.Optional(Type.String()),
34
+ persistRuns: Type.Optional(Type.Boolean()),
35
+ }),
36
+ ),
28
37
  });
29
38
  const ROOT_SETTINGS_SCHEMA = Type.Object({
30
39
  sessions: Type.Optional(SESSION_FILE_SETTINGS_SCHEMA),
@@ -41,12 +50,20 @@ export class ModelReference {
41
50
  }
42
51
  }
43
52
 
44
- export interface AutoTitleSettings {
53
+ export interface AgentModelSettings {
54
+ model?: ModelReference | undefined;
55
+ thinkingLevel?: ThinkingLevel | undefined;
56
+ }
57
+
58
+ export interface AutoTitleSettings extends AgentModelSettings {
45
59
  refreshTurns: number;
46
- model: ModelReference | undefined;
47
60
  prompt: string;
48
61
  }
49
62
 
63
+ export interface AskSettings extends AgentModelSettings {
64
+ persistRuns: boolean;
65
+ }
66
+
50
67
  export interface SessionSettings {
51
68
  handoff: {
52
69
  pickerShortcut: KeyId;
@@ -55,6 +72,7 @@ export interface SessionSettings {
55
72
  path: string;
56
73
  };
57
74
  autoTitle: AutoTitleSettings;
75
+ ask: AskSettings;
58
76
  }
59
77
 
60
78
  type SessionFileSettings = Static<typeof SESSION_FILE_SETTINGS_SCHEMA>;
@@ -67,6 +85,10 @@ export function getDefaultIndexPath(): string {
67
85
  return path.join(getDefaultIndexDir(), "index.sqlite");
68
86
  }
69
87
 
88
+ export function getDefaultSessionAskRunsDir(): string {
89
+ return path.join(getDefaultIndexDir(), "session-ask");
90
+ }
91
+
70
92
  function expandHome(rawPath: string): string {
71
93
  if (rawPath === "~") {
72
94
  return os.homedir();
@@ -102,7 +124,7 @@ function normalizePickerShortcut(value: string | undefined): KeyId {
102
124
  return (trimmed ? trimmed : "alt+o") as KeyId;
103
125
  }
104
126
 
105
- function parseModelReference(value: string | undefined): ModelReference | undefined {
127
+ export function parseModelReference(value: string | undefined): ModelReference | undefined {
106
128
  const trimmed = value?.trim();
107
129
  if (!trimmed) {
108
130
  return undefined;
@@ -116,6 +138,37 @@ function parseModelReference(value: string | undefined): ModelReference | undefi
116
138
  return new ModelReference(trimmed.slice(0, slashIndex), trimmed.slice(slashIndex + 1));
117
139
  }
118
140
 
141
+ function parseThinkingLevel(value: string | undefined): ThinkingLevel | undefined {
142
+ const trimmed = value?.trim();
143
+ switch (trimmed) {
144
+ case "off":
145
+ case "minimal":
146
+ case "low":
147
+ case "medium":
148
+ case "high":
149
+ case "xhigh":
150
+ return trimmed;
151
+ default:
152
+ return undefined;
153
+ }
154
+ }
155
+
156
+ function resolveAgentModelSettings(
157
+ value:
158
+ | {
159
+ model?: string | undefined;
160
+ thinkingLevel?: string | undefined;
161
+ }
162
+ | undefined,
163
+ ): AgentModelSettings {
164
+ const model = parseModelReference(value?.model);
165
+ const thinkingLevel = parseThinkingLevel(value?.thinkingLevel);
166
+ return {
167
+ ...(model ? { model } : {}),
168
+ ...(thinkingLevel ? { thinkingLevel } : {}),
169
+ };
170
+ }
171
+
119
172
  function normalizeAutoTitlePrompt(value: string | undefined): string {
120
173
  const trimmed = value?.trim();
121
174
  return trimmed ? trimmed : DEFAULT_AUTO_TITLE_PROMPT;
@@ -138,10 +191,14 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
138
191
  path: path.join(indexDir, "index.sqlite"),
139
192
  },
140
193
  autoTitle: {
194
+ ...resolveAgentModelSettings(fileSettings.autoTitle),
141
195
  refreshTurns: fileSettings.autoTitle?.refreshTurns ?? DEFAULT_AUTO_TITLE_REFRESH_TURNS,
142
- model: parseModelReference(fileSettings.autoTitle?.model),
143
196
  prompt: normalizeAutoTitlePrompt(fileSettings.autoTitle?.prompt),
144
197
  },
198
+ ask: {
199
+ ...resolveAgentModelSettings(fileSettings.ask),
200
+ persistRuns: fileSettings.ask?.persistRuns ?? false,
201
+ },
145
202
  };
146
203
  }
147
204
 
@@ -0,0 +1,50 @@
1
+ export function contentToText(content: unknown): string {
2
+ if (typeof content === "string") {
3
+ return content.trim();
4
+ }
5
+
6
+ if (!Array.isArray(content)) {
7
+ return "";
8
+ }
9
+
10
+ return content
11
+ .filter(isTextBlock)
12
+ .map((part) => part.text)
13
+ .join("\n")
14
+ .trim();
15
+ }
16
+
17
+ export function isRecord(value: unknown): value is Record<string, unknown> {
18
+ return typeof value === "object" && value !== null;
19
+ }
20
+
21
+ export function truncateInline(value: string, maxChars: number): string {
22
+ const cleaned = value.replace(/\s+/g, " ").trim();
23
+ if (cleaned.length <= maxChars) {
24
+ return cleaned;
25
+ }
26
+ return `${cleaned.slice(0, maxChars)}…`;
27
+ }
28
+
29
+ export function truncateBlock(
30
+ value: string,
31
+ maxChars: number,
32
+ ): { text: string; truncated: boolean } {
33
+ if (value.length <= maxChars) {
34
+ return { text: value, truncated: false };
35
+ }
36
+
37
+ return {
38
+ text: `${value.slice(0, maxChars)}\n\n[truncated to ${maxChars} characters]`,
39
+ truncated: true,
40
+ };
41
+ }
42
+
43
+ interface TextBlock {
44
+ type: "text";
45
+ text: string;
46
+ }
47
+
48
+ function isTextBlock(part: unknown): part is TextBlock {
49
+ return isRecord(part) && part.type === "text" && typeof part.text === "string";
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
6
6
  "license": "MIT",