pi-sessions 0.7.1 → 0.8.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/README.md CHANGED
@@ -190,6 +190,7 @@ To change auto-titling settings, edit `~/.pi/agent/settings.json`:
190
190
  "sessions": {
191
191
  "autoTitle": {
192
192
  "refreshTurns": 4,
193
+ "timeoutSecs": 15,
193
194
  "model": "anthropic/claude-haiku-4-5",
194
195
  "prompt": "Custom prompt that overrides the default."
195
196
  }
@@ -39,9 +39,8 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
39
39
  name: "session_ask",
40
40
  label: "Session Ask",
41
41
  description: "Interrogate a Pi session",
42
- promptSnippet:
43
- "Use when you have a session id and want to recall information, decisions, reasoning, etc from it",
44
- promptGuidelines: ["Prefer focused follow-up questions over broad recap requests"],
42
+ promptSnippet: "Recall information, decisions, or reasoning from a Pi session by id",
43
+ promptGuidelines: ["Use session_ask with focused questions rather than broad recap requests."],
45
44
  parameters: Type.Object({
46
45
  session: Type.String({
47
46
  description: "Bare UUID for the session",
@@ -7,7 +7,6 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
7
  import type { AutoTitleContext } from "./context.ts";
8
8
  import type { AutoTitleTrigger } from "./state.ts";
9
9
 
10
- const AUTO_TITLE_REQUEST_TIMEOUT_MS = 15_000;
11
10
  const AUTO_TITLE_MAX_TOKENS = 64;
12
11
  const AUTO_TITLE_CHAR_MAX = 80;
13
12
 
@@ -34,6 +33,7 @@ export async function generateAutoTitle(
34
33
  context: AutoTitleContext,
35
34
  trigger: AutoTitleTrigger,
36
35
  systemPrompt: string,
36
+ timeoutMs: number,
37
37
  ): Promise<AutoTitleGenerationResult> {
38
38
  if (!context.conversationText) {
39
39
  return {
@@ -64,7 +64,7 @@ export async function generateAutoTitle(
64
64
  };
65
65
 
66
66
  const abortController = new AbortController();
67
- const timeoutId = setTimeout(() => abortController.abort(), AUTO_TITLE_REQUEST_TIMEOUT_MS);
67
+ const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
68
68
 
69
69
  try {
70
70
  const requestContext = {
@@ -58,6 +58,7 @@ export async function runBulkRetitle(
58
58
  mode: RetitleMode,
59
59
  getSessionEpoch: () => number,
60
60
  systemPrompt: string,
61
+ timeoutMs: number,
61
62
  ): Promise<BulkRetitleResult> {
62
63
  const result: BulkRetitleResult = {
63
64
  attempted: 0,
@@ -85,6 +86,7 @@ export async function runBulkRetitle(
85
86
  model,
86
87
  isManual: true,
87
88
  systemPrompt,
89
+ timeoutMs,
88
90
  getSessionEpoch,
89
91
  notifyOnSuccess: false,
90
92
  });
@@ -92,7 +94,7 @@ export async function runBulkRetitle(
92
94
  continue;
93
95
  }
94
96
 
95
- const outcome = await retitleStoredSession(ctx, model, session.path, systemPrompt);
97
+ const outcome = await retitleStoredSession(ctx, model, session.path, systemPrompt, timeoutMs);
96
98
  result[outcome] += 1;
97
99
  }
98
100
 
@@ -165,6 +167,7 @@ export interface RetitlePlanOptions {
165
167
  model: Model<Api> | undefined;
166
168
  isManual: boolean;
167
169
  systemPrompt: string;
170
+ timeoutMs: number;
168
171
  existingPlan?: AutoTitleTriggerPlan;
169
172
  getSessionEpoch?: () => number;
170
173
  notifyOnSuccess?: boolean;
@@ -173,8 +176,17 @@ export interface RetitlePlanOptions {
173
176
  export async function runRetitlePlan(
174
177
  options: RetitlePlanOptions,
175
178
  ): Promise<AutoTitleGenerationResult> {
176
- const { pi, controller, ctx, model, isManual, systemPrompt, existingPlan, getSessionEpoch } =
177
- options;
179
+ const {
180
+ pi,
181
+ controller,
182
+ ctx,
183
+ model,
184
+ isManual,
185
+ systemPrompt,
186
+ timeoutMs,
187
+ existingPlan,
188
+ getSessionEpoch,
189
+ } = options;
178
190
  const notifyOnSuccess = options.notifyOnSuccess ?? isManual;
179
191
 
180
192
  const plan = existingPlan ?? controller.handleManualRetitle(ctx);
@@ -211,6 +223,7 @@ export async function runRetitlePlan(
211
223
  titleContext,
212
224
  plan.reason,
213
225
  systemPrompt,
226
+ timeoutMs,
214
227
  );
215
228
  if (!generatedTitle.ok) {
216
229
  return generatedTitle;
@@ -257,6 +270,7 @@ async function retitleStoredSession(
257
270
  model: Model<Api>,
258
271
  sessionPath: string,
259
272
  systemPrompt: string,
273
+ timeoutMs: number,
260
274
  ): Promise<"retitled" | "unchanged" | "failed"> {
261
275
  const sessionManager = SessionManager.open(sessionPath);
262
276
  const currentTitle = sessionManager.getSessionName();
@@ -279,6 +293,7 @@ async function retitleStoredSession(
279
293
  titleContext,
280
294
  plan.reason,
281
295
  systemPrompt,
296
+ timeoutMs,
282
297
  );
283
298
  if (!generatedTitle.ok) {
284
299
  return "failed";
@@ -57,6 +57,7 @@ export async function showRetitleWizard(
57
57
  model: Model<Api> | undefined,
58
58
  getSessionEpoch: () => number,
59
59
  systemPrompt: string,
60
+ timeoutMs: number,
60
61
  options?: RetitleWizardOptions,
61
62
  ): Promise<RetitleCommandOutcome> {
62
63
  return ctx.ui.custom<RetitleCommandOutcome>(
@@ -70,6 +71,7 @@ export async function showRetitleWizard(
70
71
  model,
71
72
  getSessionEpoch,
72
73
  systemPrompt,
74
+ timeoutMs,
73
75
  done,
74
76
  options,
75
77
  ),
@@ -99,6 +101,7 @@ class RetitleWizardPanel implements Focusable {
99
101
  private readonly model: Model<Api> | undefined,
100
102
  private readonly getSessionEpoch: () => number,
101
103
  private readonly systemPrompt: string,
104
+ private readonly timeoutMs: number,
102
105
  private readonly done: (result: RetitleCommandOutcome) => void,
103
106
  options?: RetitleWizardOptions,
104
107
  ) {
@@ -250,6 +253,7 @@ class RetitleWizardPanel implements Focusable {
250
253
  model: this.model,
251
254
  isManual: true,
252
255
  systemPrompt: this.systemPrompt,
256
+ timeoutMs: this.timeoutMs,
253
257
  getSessionEpoch: this.getSessionEpoch,
254
258
  });
255
259
  if (result.ok) {
@@ -310,6 +314,7 @@ class RetitleWizardPanel implements Focusable {
310
314
  mode,
311
315
  this.getSessionEpoch,
312
316
  this.systemPrompt,
317
+ this.timeoutMs,
313
318
  );
314
319
  notifyBulkRetitleResult(this.ctx, scan, mode, result);
315
320
  this.done(
@@ -75,6 +75,7 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
75
75
  model,
76
76
  invocation,
77
77
  settings.autoTitle.prompt,
78
+ settings.autoTitle.timeoutMs,
78
79
  );
79
80
  },
80
81
  ),
@@ -104,6 +105,7 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
104
105
  getSessionEpoch: () => sessionEpoch,
105
106
  notifyOnSuccess: false,
106
107
  systemPrompt: settings.autoTitle.prompt,
108
+ timeoutMs: settings.autoTitle.timeoutMs,
107
109
  })
108
110
  .then((outcome) => {
109
111
  if (outcome.ok) {
@@ -139,6 +141,7 @@ async function handleTitleInvocation(
139
141
  model: Model<Api> | undefined,
140
142
  invocation: RetitleCommandInvocation,
141
143
  systemPrompt: string,
144
+ timeoutMs: number,
142
145
  ): Promise<RetitleCommandOutcome> {
143
146
  const retitleOpts = {
144
147
  pi,
@@ -147,6 +150,7 @@ async function handleTitleInvocation(
147
150
  model,
148
151
  isManual: true,
149
152
  systemPrompt,
153
+ timeoutMs,
150
154
  getSessionEpoch: state.getSessionEpoch,
151
155
  };
152
156
 
@@ -165,7 +169,15 @@ async function handleTitleInvocation(
165
169
  return retitleCurrentSession();
166
170
  }
167
171
 
168
- return showRetitleWizard(pi, state.controller, ctx, model, state.getSessionEpoch, systemPrompt);
172
+ return showRetitleWizard(
173
+ pi,
174
+ state.controller,
175
+ ctx,
176
+ model,
177
+ state.getSessionEpoch,
178
+ systemPrompt,
179
+ timeoutMs,
180
+ );
169
181
  }
170
182
 
171
183
  if (invocation.scope === "this") {
@@ -180,6 +192,7 @@ async function handleTitleInvocation(
180
192
  model,
181
193
  state.getSessionEpoch,
182
194
  systemPrompt,
195
+ timeoutMs,
183
196
  {
184
197
  initialInvocation: {
185
198
  scope: invocation.scope,
@@ -201,6 +214,7 @@ async function handleTitleInvocation(
201
214
  mode,
202
215
  state.getSessionEpoch,
203
216
  systemPrompt,
217
+ timeoutMs,
204
218
  );
205
219
  notifyBulkRetitleResult(ctx, scan, mode, result);
206
220
  return "success";
@@ -87,16 +87,15 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
87
87
  "Start a background pi session in a terminal split based on the current session",
88
88
  promptGuidelines: [
89
89
  "Use session_handoff only when it is clear the work should be forked to a new context.",
90
- "Prefer using session_handoff by direction of the user, not as an unsolicited default.",
91
- "The goal should capture enough detail to encompass the ask and include any directions the next sessions should consider.",
92
- "Only request a response when there is a specific ask-and-response expectation: the user asked for a report back, or the current session needs the child result before it can continue. Do not set requestResponse for independent or unrelated background work; most split-off tasks should run without reporting back.",
93
- "Only capable of a background handoff; to replace the current session, tell the user to run /handoff instead.",
90
+ "Reach for session_handoff by direction of the user, not as an unsolicited default.",
91
+ "session_handoff should only request a response when there is a specific ask-and-response expectation: the user asked for a report back, or this session needs the child result to continue. Leave it off by default and for independent background work.",
92
+ "session_handoff can only fork a background session; to replace the current session, tell the user to run /handoff instead.",
94
93
  ],
95
94
  executionMode: "sequential",
96
95
  parameters: Type.Object({
97
96
  goal: Type.String({
98
97
  description:
99
- "Goal for the new session, including enough detail to capture the user's ask and any directions the next prompt should consider.",
98
+ "Goal for the new session. Capture enough detail to encompass the ask and any directions the next session should consider.",
100
99
  }),
101
100
  splitDirection: Type.Union(
102
101
  [Type.Literal("left"), Type.Literal("right"), Type.Literal("up"), Type.Literal("down")],
@@ -14,7 +14,7 @@ export function registerSessionMessagingTools(
14
14
  description: "Send a message to another live pi session",
15
15
  promptSnippet: "Send a message to another live pi session",
16
16
  promptGuidelines: [
17
- "Use session_search with live: true to discover targetable sessions. Usually it is best to exclude all other filters and simply get a list of all live sessions",
17
+ "Before session_send_message, use session_search with live: true (no other filters) to list all live sessions and find the target.",
18
18
  ],
19
19
  parameters: SEND_MESSAGE_PARAMS,
20
20
  renderCall(args, theme, context) {
@@ -64,10 +64,10 @@ export default function sessionSearchExtension(
64
64
  name: "session_search",
65
65
  label: "Session Search",
66
66
  description: "Search Pi sessions",
67
- promptSnippet: "Use when you need to locate a session to do a detailed follow-up",
67
+ promptSnippet: "Locate Pi sessions for follow-up",
68
68
  promptGuidelines: [
69
- "Omit query to list matching sessions chronologically",
70
- "Once you have the right session id, switch to session_ask for questions about that session",
69
+ "Omit queries in session_search to list matching sessions chronologically.",
70
+ "After session_search finds a session id, switch to session_ask for questions about it.",
71
71
  ],
72
72
  parameters: Type.Object({
73
73
  query: Type.Optional(
@@ -7,6 +7,7 @@ import { type Static, Type } from "typebox";
7
7
  import { parseTypeBoxValue } from "./typebox.ts";
8
8
 
9
9
  export const DEFAULT_AUTO_TITLE_REFRESH_TURNS = 4;
10
+ export const DEFAULT_AUTO_TITLE_TIMEOUT_SECONDS = 15;
10
11
  export const DEFAULT_AUTO_TITLE_PROMPT = `Name this coding session (under 80 chars). Be specific to what is being discussed. Your exact output will be displayed to the user, so make sure that it contains ONLY the title itself and nothing else.`;
11
12
  const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
12
13
  handoff: Type.Optional(
@@ -22,6 +23,7 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
22
23
  autoTitle: Type.Optional(
23
24
  Type.Object({
24
25
  refreshTurns: Type.Optional(Type.Integer({ minimum: 1 })),
26
+ timeoutSecs: Type.Optional(Type.Integer({ minimum: 1 })),
25
27
  model: Type.Optional(Type.String()),
26
28
  thinkingLevel: Type.Optional(Type.String()),
27
29
  prompt: Type.Optional(Type.String()),
@@ -57,6 +59,7 @@ export interface AgentModelSettings {
57
59
 
58
60
  export interface AutoTitleSettings extends AgentModelSettings {
59
61
  refreshTurns: number;
62
+ timeoutMs: number;
60
63
  prompt: string;
61
64
  }
62
65
 
@@ -193,6 +196,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
193
196
  autoTitle: {
194
197
  ...resolveAgentModelSettings(fileSettings.autoTitle),
195
198
  refreshTurns: fileSettings.autoTitle?.refreshTurns ?? DEFAULT_AUTO_TITLE_REFRESH_TURNS,
199
+ timeoutMs: (fileSettings.autoTitle?.timeoutSecs ?? DEFAULT_AUTO_TITLE_TIMEOUT_SECONDS) * 1000,
196
200
  prompt: normalizeAutoTitlePrompt(fileSettings.autoTitle?.prompt),
197
201
  },
198
202
  ask: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
6
6
  "license": "MIT",