@fickydev/pigent 0.1.13 → 0.1.14

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/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.14 - 2026-05-18
6
+
5
7
  ### Added
6
8
 
7
9
  - Added `task_runs` table and `TaskRepository` for scheduled task persistence.
@@ -61,6 +63,8 @@
61
63
  - Added `/task` to Telegram bot command list (shows subcommands in help text).
62
64
  - Added `/new` Telegram command to start a fresh active chat session.
63
65
  - Added `/status` Telegram command with session/model/workspace/task info and estimated context usage.
66
+ - Added persistent Pi session files per Pigent session so chat memory survives across messages and daemon restarts.
67
+ - Added Pi session metadata to `/status`, with real Pi context usage when available.
64
68
  - Changed `/model` picker to use Pi's currently available models automatically when explicit `modelChoices` are not configured.
65
69
  - Kept daemon process alive after startup so CLI runs do not exit after `pigent ready`.
66
70
 
package/README.md CHANGED
@@ -380,6 +380,7 @@ Planned commands:
380
380
  - Keep Telegram-specific logic inside `src/channels/telegram` or routing config.
381
381
  - Channel adapters must not call Pi directly.
382
382
  - Pi runner must not know channel secrets.
383
+ - Pi sessions are persisted under `~/.pigent/pi-sessions` by default (`PIGENT_PI_SESSION_DIR` overrides this).
383
384
  - Use repositories for database access.
384
385
  - Keep SQLite/PostgreSQL portability in mind.
385
386
  - Never inject secrets or raw `.env` values into model prompts.
package/TODO.md CHANGED
@@ -139,7 +139,9 @@
139
139
  - [x] Apply session model override
140
140
  - [x] Apply thinking level selection
141
141
  - [x] Confirm SDK APIs needed for session creation
142
- - [ ] Confirm persistent session manager approach
142
+ - [x] Confirm persistent session manager approach
143
+ - [x] Persist Pi session file path per active Pigent session
144
+ - [x] Reuse Pi session files across messages
143
145
  - [ ] Confirm per-agent system prompt injection
144
146
  - [ ] Confirm per-agent skills loading
145
147
  - [ ] Confirm per-agent extensions loading
@@ -0,0 +1 @@
1
+ ALTER TABLE `agent_sessions` ADD `pi_session_path` text;
@@ -36,6 +36,13 @@
36
36
  "when": 1779099600000,
37
37
  "tag": "0004_active_agent_sessions",
38
38
  "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "6",
43
+ "when": 1779099900000,
44
+ "tag": "0005_pi_session_path",
45
+ "breakpoints": true
39
46
  }
40
47
  ]
41
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fickydev/pigent",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Autonomous multi-agent daemon using Pi as core execution engine.",
@@ -104,14 +104,21 @@ export class AgentRunner {
104
104
  if (!agent) return { text: `Unknown agent: ${input.agentId}` };
105
105
 
106
106
  try {
107
- const text = await this.piRunner.run({
107
+ const result = await this.piRunner.run({
108
108
  agent,
109
109
  profile: this.registry.getProfile(agent.profile),
110
110
  session,
111
111
  prompt: this.composePrompt(input, chatInstructions),
112
112
  });
113
113
 
114
- return { text };
114
+ if (session.piSessionId !== result.piSessionId || session.piSessionPath !== result.piSessionPath) {
115
+ await this.repositories.sessions.updatePiSession(session.id, {
116
+ piSessionId: result.piSessionId,
117
+ piSessionPath: result.piSessionPath,
118
+ });
119
+ }
120
+
121
+ return { text: result.text };
115
122
  } catch (error) {
116
123
  const errorMessage = error instanceof Error ? error.message : String(error);
117
124
 
@@ -1,7 +1,9 @@
1
1
  import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
  import type { InboundMessage, InlineKeyboardButton } from "../channels/types";
3
- import type { ModelChoiceConfig } from "../config/schemas";
3
+ import type { LoadedAgentConfig, ModelChoiceConfig, ProfileConfig } from "../config/schemas";
4
+ import type { AgentSessionRow } from "../db/schema";
4
5
  import type { Repositories } from "../db/repositories";
6
+ import { PiAgentRunner, type PiContextUsage } from "../pi/PiAgentRunner";
5
7
  import { getAvailableModelChoices } from "../pi/PiAvailableModels";
6
8
  import { isValidModelRef, resolveModelSelection } from "../pi/PiModelResolver";
7
9
  import type { AgentRegistry } from "./AgentRegistry";
@@ -21,6 +23,8 @@ const THINKING_LEVELS = ["off", "low", "medium", "high"] as const;
21
23
  type ThinkingLevel = (typeof THINKING_LEVELS)[number];
22
24
 
23
25
  export class BotCommandHandler {
26
+ private readonly piRunner = new PiAgentRunner();
27
+
24
28
  constructor(
25
29
  private readonly registry: AgentRegistry,
26
30
  private readonly repositories: Repositories,
@@ -134,7 +138,11 @@ export class BotCommandHandler {
134
138
  const profile = this.registry.getProfile(agent.profile);
135
139
  const messageStats = await this.repositories.messages.statsBySession(sessionResult.session.id);
136
140
  const modelInfo = this.resolveStatusModel(sessionResult.session, agent, profile);
137
- const contextText = formatContext(messageStats.estimatedTokens, modelInfo.contextWindow);
141
+ const piStatus = await this.loadPiStatus(sessionResult.session, agent, profile);
142
+ const contextText = piStatus?.contextUsage
143
+ ? formatPiContext(piStatus.contextUsage)
144
+ : formatEstimatedContext(messageStats.estimatedTokens, modelInfo.contextWindow);
145
+ const contextSource = piStatus?.contextUsage ? "Pi session" : "Pigent DB estimate";
138
146
  const taskCount = this.scheduler
139
147
  ? this.scheduler.getTasks().filter((task) => task.chatId === message.chatId || !task.chatId).length
140
148
  : null;
@@ -151,6 +159,8 @@ export class BotCommandHandler {
151
159
  "Model: " + modelInfo.model,
152
160
  "Thinking: " + (sessionResult.session.thinkingLevel ?? agent.thinkingLevel ?? profile?.thinkingLevel ?? "default"),
153
161
  "Context: " + contextText,
162
+ "Context source: " + contextSource,
163
+ "Pi session: " + (piStatus?.piSessionId ?? sessionResult.session.piSessionId ?? "not created"),
154
164
  "Workspace: " + formatPath(agent.workspace),
155
165
  "Chat: " + message.chatId,
156
166
  "Thread: " + (message.threadId ?? "none"),
@@ -159,6 +169,14 @@ export class BotCommandHandler {
159
169
  ].join("\n");
160
170
  }
161
171
 
172
+ private async loadPiStatus(session: AgentSessionRow, agent: LoadedAgentConfig, profile: ProfileConfig | null) {
173
+ try {
174
+ return await this.piRunner.status({ agent, profile, session });
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
162
180
  private resolveStatusModel(session: { model: string | null }, agent: { model?: string | null }, profile: { model?: string | null } | null) {
163
181
  const configuredModel = session.model ?? agent.model ?? profile?.model ?? null;
164
182
  if (!configuredModel) return { model: "default", contextWindow: null as number | null };
@@ -455,7 +473,15 @@ function chunk<T>(items: T[], size: number): T[][] {
455
473
  return rows;
456
474
  }
457
475
 
458
- function formatContext(tokens: number, contextWindow: number | null): string {
476
+ function formatPiContext(contextUsage: PiContextUsage): string {
477
+ if (contextUsage.percent === null || contextUsage.tokens === null) {
478
+ return "? / " + formatTokens(contextUsage.contextWindow);
479
+ }
480
+
481
+ return contextUsage.percent.toFixed(1) + "% / " + formatTokens(contextUsage.contextWindow);
482
+ }
483
+
484
+ function formatEstimatedContext(tokens: number, contextWindow: number | null): string {
459
485
  if (!contextWindow || contextWindow <= 0) {
460
486
  return "~" + formatTokens(tokens) + " tokens estimated / unknown limit";
461
487
  }
@@ -62,6 +62,19 @@ export class SessionRepository {
62
62
  return this.requireById(id);
63
63
  }
64
64
 
65
+ async updatePiSession(id: string, input: { piSessionId: string; piSessionPath: string }): Promise<AgentSessionRow> {
66
+ await this.db
67
+ .update(agentSessions)
68
+ .set({
69
+ piSessionId: input.piSessionId,
70
+ piSessionPath: input.piSessionPath,
71
+ updatedAt: Date.now(),
72
+ })
73
+ .where(eq(agentSessions.id, id));
74
+
75
+ return this.requireById(id);
76
+ }
77
+
65
78
  async findById(id: string): Promise<AgentSessionRow | null> {
66
79
  const row = await this.db.query.agentSessions.findFirst({
67
80
  where: eq(agentSessions.id, id),
package/src/db/schema.ts CHANGED
@@ -47,6 +47,7 @@ export const agentSessions = sqliteTable(
47
47
  threadId: text("thread_id"),
48
48
  userId: text("user_id"),
49
49
  piSessionId: text("pi_session_id"),
50
+ piSessionPath: text("pi_session_path"),
50
51
  instructionsHash: text("instructions_hash"),
51
52
  model: text("model"),
52
53
  thinkingLevel: text("thinking_level", { enum: ["off", "low", "medium", "high"] }),
@@ -4,7 +4,6 @@ import {
4
4
  DefaultResourceLoader,
5
5
  ModelRegistry,
6
6
  getAgentDir,
7
- SessionManager,
8
7
  SettingsManager,
9
8
  } from "@earendil-works/pi-coding-agent";
10
9
  import { mkdir } from "node:fs/promises";
@@ -12,6 +11,7 @@ import { resolve } from "node:path";
12
11
  import type { LoadedAgentConfig, ProfileConfig } from "../config/schemas";
13
12
  import type { AgentSessionRow } from "../db/schema";
14
13
  import { resolveModelSelection } from "./PiModelResolver";
14
+ import { loadOrCreatePiSession } from "./PiSessionFactory";
15
15
 
16
16
  export type PiAgentRunInput = {
17
17
  agent: LoadedAgentConfig;
@@ -20,8 +20,58 @@ export type PiAgentRunInput = {
20
20
  prompt: string;
21
21
  };
22
22
 
23
+ export type PiContextUsage = {
24
+ tokens: number | null;
25
+ contextWindow: number;
26
+ percent: number | null;
27
+ };
28
+
29
+ export type PiAgentRunResult = {
30
+ text: string;
31
+ piSessionId: string;
32
+ piSessionPath: string;
33
+ contextUsage?: PiContextUsage;
34
+ };
35
+
36
+ export type PiSessionStatus = {
37
+ piSessionId: string;
38
+ piSessionPath: string;
39
+ contextUsage?: PiContextUsage;
40
+ };
41
+
23
42
  export class PiAgentRunner {
24
- async run(input: PiAgentRunInput): Promise<string> {
43
+ async run(input: PiAgentRunInput): Promise<PiAgentRunResult> {
44
+ const prepared = await this.prepare(input);
45
+
46
+ let response = "";
47
+ prepared.session.subscribe((event) => {
48
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
49
+ response += event.assistantMessageEvent.delta;
50
+ }
51
+ });
52
+
53
+ await prepared.session.prompt(input.prompt);
54
+
55
+ return {
56
+ text: response.trim() || "(no response)",
57
+ piSessionId: prepared.piSession.sessionId,
58
+ piSessionPath: prepared.piSession.sessionPath,
59
+ contextUsage: prepared.session.getContextUsage(),
60
+ };
61
+ }
62
+
63
+ async status(input: Omit<PiAgentRunInput, "prompt">): Promise<PiSessionStatus | null> {
64
+ if (!input.session.piSessionPath) return null;
65
+
66
+ const prepared = await this.prepare({ ...input, prompt: "" });
67
+ return {
68
+ piSessionId: prepared.piSession.sessionId,
69
+ piSessionPath: prepared.piSession.sessionPath,
70
+ contextUsage: prepared.session.getContextUsage(),
71
+ };
72
+ }
73
+
74
+ private async prepare(input: PiAgentRunInput) {
25
75
  const workspace = resolve(input.agent.workspace);
26
76
  await mkdir(workspace, { recursive: true });
27
77
 
@@ -41,6 +91,7 @@ export class PiAgentRunner {
41
91
  const authStorage = AuthStorage.create();
42
92
  const modelRegistry = ModelRegistry.create(authStorage);
43
93
  const modelSelection = resolveModelSelection(modelRegistry, [input.session, input.agent, input.profile ?? {}]);
94
+ const piSession = await loadOrCreatePiSession(workspace, input.session);
44
95
  const { session } = await createAgentSession({
45
96
  cwd: workspace,
46
97
  authStorage,
@@ -48,22 +99,13 @@ export class PiAgentRunner {
48
99
  model: modelSelection.model,
49
100
  thinkingLevel: modelSelection.thinkingLevel,
50
101
  resourceLoader,
51
- sessionManager: SessionManager.create(workspace),
102
+ sessionManager: piSession.manager,
52
103
  settingsManager,
53
104
  agentDir,
54
105
  tools: [],
55
106
  });
56
107
 
57
- let response = "";
58
- session.subscribe((event) => {
59
- if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
60
- response += event.assistantMessageEvent.delta;
61
- }
62
- });
63
-
64
- await session.prompt(input.prompt);
65
-
66
- return response.trim() || "(no response)";
108
+ return { session, piSession };
67
109
  }
68
110
  }
69
111
 
@@ -0,0 +1,49 @@
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import type { AgentSessionRow } from "../db/schema";
7
+
8
+ export type PiSessionHandle = {
9
+ manager: SessionManager;
10
+ sessionId: string;
11
+ sessionPath: string;
12
+ created: boolean;
13
+ };
14
+
15
+ export async function loadOrCreatePiSession(workspace: string, session: AgentSessionRow): Promise<PiSessionHandle> {
16
+ const sessionDir = piSessionDir();
17
+ await mkdir(sessionDir, { recursive: true });
18
+
19
+ if (session.piSessionPath && existsSync(session.piSessionPath)) {
20
+ const manager = SessionManager.open(session.piSessionPath, sessionDir, workspace);
21
+ return {
22
+ manager,
23
+ sessionId: manager.getSessionId(),
24
+ sessionPath: manager.getSessionFile() ?? session.piSessionPath,
25
+ created: false,
26
+ };
27
+ }
28
+
29
+ const manager = SessionManager.create(workspace, sessionDir);
30
+ manager.newSession({ id: session.piSessionId ?? session.id });
31
+
32
+ const sessionPath = manager.getSessionFile();
33
+ if (!sessionPath) {
34
+ throw new Error("failed to create persisted Pi session file");
35
+ }
36
+
37
+ await mkdir(dirname(sessionPath), { recursive: true });
38
+
39
+ return {
40
+ manager,
41
+ sessionId: manager.getSessionId(),
42
+ sessionPath,
43
+ created: true,
44
+ };
45
+ }
46
+
47
+ export function piSessionDir(): string {
48
+ return resolve(process.env.PIGENT_PI_SESSION_DIR ?? join(process.env.PIGENT_HOME ?? join(homedir(), ".pigent"), "pi-sessions"));
49
+ }