@hienlh/ppm 0.9.48 โ 0.9.50
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
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.50] - 2026-04-07
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **/sessions redesigned**: Filters by current project, shows pinned sessions first with ๐ icon, displays session titles (not project names), supports pagination via `/sessions 2`. Matches PPM web UI behavior.
|
|
7
|
+
|
|
8
|
+
## [0.9.49] - 2026-04-07
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **/resume accepts session ID prefix**: `/resume fdc4ddaa` now works alongside `/resume 2` (index). Matches session by ID prefix from `/sessions` list.
|
|
12
|
+
- **/restart actually restarts**: Server now exits with code 42 (restart signal) instead of 0 (clean exit). Supervisor recognizes code 42 and respawns immediately without backoff.
|
|
13
|
+
- **Restart notification delivered**: Supervisor respawns after `/restart`, new server sends "PPM v0.9.49 restarted successfully." to all paired chats.
|
|
14
|
+
- **/project lists all projects**: `getProjectNames()` now merges config projects + unique project names from session history. Previously returned empty when no projects in config.
|
|
15
|
+
|
|
3
16
|
## [0.9.48] - 2026-04-06
|
|
4
17
|
|
|
5
18
|
### Changed
|
package/package.json
CHANGED
|
@@ -1022,6 +1022,13 @@ export function getRecentPPMBotSessions(
|
|
|
1022
1022
|
).all(telegramChatId, limit) as PPMBotSessionRow[];
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
+
export function getDistinctPPMBotProjectNames(): string[] {
|
|
1026
|
+
const rows = getDb().query(
|
|
1027
|
+
"SELECT DISTINCT project_name FROM clawbot_sessions ORDER BY project_name",
|
|
1028
|
+
).all() as { project_name: string }[];
|
|
1029
|
+
return rows.map((r) => r.project_name);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1025
1032
|
// ---------------------------------------------------------------------------
|
|
1026
1033
|
// PPMBot memory helpers
|
|
1027
1034
|
// ---------------------------------------------------------------------------
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getPairingByChatId,
|
|
6
6
|
createPairingRequest,
|
|
7
7
|
getSessionTitles,
|
|
8
|
+
getPinnedSessionIds,
|
|
8
9
|
getApprovedPairedChats,
|
|
9
10
|
} from "../db.service.ts";
|
|
10
11
|
import { PPMBotTelegram } from "./ppmbot-telegram.ts";
|
|
@@ -159,7 +160,7 @@ class PPMBotService {
|
|
|
159
160
|
case "start": await this.cmdStart(chatId); break;
|
|
160
161
|
case "project": await this.cmdProject(chatId, cmd.args); break;
|
|
161
162
|
case "new": await this.cmdNew(chatId); break;
|
|
162
|
-
case "sessions": await this.cmdSessions(chatId); break;
|
|
163
|
+
case "sessions": await this.cmdSessions(chatId, cmd.args); break;
|
|
163
164
|
case "resume": await this.cmdResume(chatId, cmd.args); break;
|
|
164
165
|
case "status": await this.cmdStatus(chatId); break;
|
|
165
166
|
case "stop": await this.cmdStop(chatId); break;
|
|
@@ -254,39 +255,90 @@ class PPMBotService {
|
|
|
254
255
|
);
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
private async cmdSessions(chatId: string): Promise<void> {
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
private async cmdSessions(chatId: string, args: string): Promise<void> {
|
|
259
|
+
const PAGE_SIZE = 8;
|
|
260
|
+
const page = Math.max(1, parseInt(args, 10) || 1);
|
|
261
|
+
|
|
262
|
+
const active = this.sessions.getActiveSession(chatId);
|
|
263
|
+
const project = active?.projectName;
|
|
264
|
+
|
|
265
|
+
// Fetch all sessions for this chat (enough for pagination)
|
|
266
|
+
const allSessions = this.sessions.listRecentSessions(chatId, 50);
|
|
267
|
+
// Filter by current project if one is active
|
|
268
|
+
const filtered = project
|
|
269
|
+
? allSessions.filter((s) => s.project_name === project)
|
|
270
|
+
: allSessions;
|
|
271
|
+
|
|
272
|
+
if (filtered.length === 0) {
|
|
273
|
+
await this.telegram!.sendMessage(Number(chatId), "No sessions yet. Send a message to start.");
|
|
261
274
|
return;
|
|
262
275
|
}
|
|
263
276
|
|
|
264
|
-
//
|
|
265
|
-
const titles = getSessionTitles(
|
|
277
|
+
// Enrich with titles and pin status
|
|
278
|
+
const titles = getSessionTitles(filtered.map((s) => s.session_id));
|
|
279
|
+
const pinnedIds = getPinnedSessionIds();
|
|
280
|
+
|
|
281
|
+
// Sort: pinned first, then by last_message_at desc
|
|
282
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
283
|
+
const aPin = pinnedIds.has(a.session_id) ? 1 : 0;
|
|
284
|
+
const bPin = pinnedIds.has(b.session_id) ? 1 : 0;
|
|
285
|
+
if (aPin !== bPin) return bPin - aPin;
|
|
286
|
+
return b.last_message_at - a.last_message_at;
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const totalPages = Math.ceil(sorted.length / PAGE_SIZE);
|
|
290
|
+
const start = (page - 1) * PAGE_SIZE;
|
|
291
|
+
const pageItems = sorted.slice(start, start + PAGE_SIZE);
|
|
292
|
+
|
|
293
|
+
if (pageItems.length === 0) {
|
|
294
|
+
await this.telegram!.sendMessage(Number(chatId), `No sessions on page ${page}.`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
266
297
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
298
|
+
const header = project ? escapeHtml(project) : "All Projects";
|
|
299
|
+
let text = `<b>Sessions โ ${header}</b>`;
|
|
300
|
+
if (totalPages > 1) text += ` <i>(${page}/${totalPages})</i>`;
|
|
301
|
+
text += "\n\n";
|
|
302
|
+
|
|
303
|
+
pageItems.forEach((s, i) => {
|
|
304
|
+
const pin = pinnedIds.has(s.session_id) ? "๐ " : "";
|
|
305
|
+
const activeDot = s.is_active ? " โฌค" : "";
|
|
306
|
+
const rawTitle = titles[s.session_id]?.replace(/^\[PPM\]\s*/, "") || "";
|
|
307
|
+
const title = rawTitle
|
|
308
|
+
? escapeHtml(rawTitle.slice(0, 45))
|
|
309
|
+
: "<i>untitled</i>";
|
|
272
310
|
const date = new Date(s.last_message_at * 1000).toLocaleString(undefined, {
|
|
273
311
|
month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
|
|
274
312
|
});
|
|
275
313
|
const sid = s.session_id.slice(0, 8);
|
|
276
|
-
|
|
314
|
+
const num = start + i + 1;
|
|
315
|
+
|
|
316
|
+
text += `${pin}${num}. ${title}${activeDot}\n`;
|
|
277
317
|
text += ` <code>${sid}</code> ยท ${date}\n\n`;
|
|
278
318
|
});
|
|
279
|
-
|
|
319
|
+
|
|
320
|
+
text += "Resume: /resume <n> or /resume <id>";
|
|
321
|
+
if (totalPages > 1 && page < totalPages) {
|
|
322
|
+
text += `\nNext: /sessions ${page + 1}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
280
325
|
await this.telegram!.sendMessage(Number(chatId), text);
|
|
281
326
|
}
|
|
282
327
|
|
|
283
328
|
private async cmdResume(chatId: string, args: string): Promise<void> {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
await this.telegram!.sendMessage(Number(chatId), "Usage: /resume <number>");
|
|
329
|
+
if (!args.trim()) {
|
|
330
|
+
await this.telegram!.sendMessage(Number(chatId), "Usage: /resume <number or session-id>");
|
|
287
331
|
return;
|
|
288
332
|
}
|
|
289
|
-
|
|
333
|
+
|
|
334
|
+
// Support both index (e.g. "2") and session ID prefix (e.g. "fdc4ddaa")
|
|
335
|
+
const index = parseInt(args, 10);
|
|
336
|
+
const isIndex = !isNaN(index) && index >= 1 && String(index) === args.trim();
|
|
337
|
+
|
|
338
|
+
const session = isIndex
|
|
339
|
+
? await this.sessions.resumeSessionById(chatId, index)
|
|
340
|
+
: await this.sessions.resumeSessionByIdPrefix(chatId, args.trim());
|
|
341
|
+
|
|
290
342
|
if (!session) {
|
|
291
343
|
await this.telegram!.sendMessage(Number(chatId), "Session not found.");
|
|
292
344
|
return;
|
|
@@ -368,8 +420,8 @@ class PPMBotService {
|
|
|
368
420
|
const markerPath = join(homedir(), ".ppm", "restart-notify.json");
|
|
369
421
|
writeFileSync(markerPath, JSON.stringify({ chatIds, ts: Date.now() }));
|
|
370
422
|
|
|
371
|
-
console.log("[ppmbot] Restart requested via Telegram, exiting...");
|
|
372
|
-
process.exit(
|
|
423
|
+
console.log("[ppmbot] Restart requested via Telegram, exiting with code 42...");
|
|
424
|
+
process.exit(42);
|
|
373
425
|
}, 500);
|
|
374
426
|
}
|
|
375
427
|
|
|
@@ -420,8 +472,8 @@ class PPMBotService {
|
|
|
420
472
|
/start โ Greeting + list projects
|
|
421
473
|
/project <name> โ Switch/list projects
|
|
422
474
|
/new โ Fresh session (current project)
|
|
423
|
-
/sessions โ List
|
|
424
|
-
/resume <n> โ Resume session
|
|
475
|
+
/sessions [page] โ List sessions (current project)
|
|
476
|
+
/resume <n or id> โ Resume session
|
|
425
477
|
/status โ Current project/session info
|
|
426
478
|
/stop โ End current session
|
|
427
479
|
/memory โ Show project memories
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
deactivatePPMBotSession,
|
|
10
10
|
touchPPMBotSession,
|
|
11
11
|
getRecentPPMBotSessions,
|
|
12
|
+
getDistinctPPMBotProjectNames,
|
|
12
13
|
setSessionTitle,
|
|
13
14
|
} from "../db.service.ts";
|
|
14
15
|
import type { PPMBotActiveSession, PPMBotSessionRow } from "../../types/ppmbot.ts";
|
|
@@ -93,6 +94,23 @@ export class PPMBotSessionManager {
|
|
|
93
94
|
return this.resumeFromDb(chatId, target, project);
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/** Resume a session by session ID prefix match */
|
|
98
|
+
async resumeSessionByIdPrefix(
|
|
99
|
+
chatId: string,
|
|
100
|
+
prefix: string,
|
|
101
|
+
): Promise<PPMBotActiveSession | null> {
|
|
102
|
+
const sessions = getRecentPPMBotSessions(chatId, 20);
|
|
103
|
+
const target = sessions.find((s) => s.session_id.startsWith(prefix));
|
|
104
|
+
if (!target) return null;
|
|
105
|
+
|
|
106
|
+
await this.closeSession(chatId);
|
|
107
|
+
|
|
108
|
+
const project = this.resolveProject(target.project_name);
|
|
109
|
+
if (!project) return null;
|
|
110
|
+
|
|
111
|
+
return this.resumeFromDb(chatId, target, project);
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
/**
|
|
97
115
|
* Resolve a project name against configured projects.
|
|
98
116
|
* Case-insensitive, supports prefix matching.
|
|
@@ -119,10 +137,12 @@ export class PPMBotSessionManager {
|
|
|
119
137
|
setSessionTitle(sessionId, title);
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
/** Get list of available project names (
|
|
140
|
+
/** Get list of available project names (config + sessions history) */
|
|
123
141
|
getProjectNames(): string[] {
|
|
124
|
-
const
|
|
125
|
-
|
|
142
|
+
const configured = (configService.get("projects") as ProjectConfig[])?.map((p) => p.name) ?? [];
|
|
143
|
+
const fromSessions = getDistinctPPMBotProjectNames();
|
|
144
|
+
const merged = new Set([...configured, ...fromSessions]);
|
|
145
|
+
return [...merged].sort();
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
// โโ Private โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -130,11 +130,17 @@ export async function spawnServer(
|
|
|
130
130
|
const exitCode = await serverChild.exited;
|
|
131
131
|
serverChild = null;
|
|
132
132
|
|
|
133
|
-
if (exitCode === 0
|
|
133
|
+
if (exitCode === 0 && shuttingDown) {
|
|
134
134
|
log("INFO", `Server exited cleanly (code ${exitCode})`);
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// Exit code 42 = restart requested (e.g. /restart from Telegram)
|
|
139
|
+
if (exitCode === 42 || (exitCode === 0 && !shuttingDown)) {
|
|
140
|
+
log("INFO", `Server restart requested (code ${exitCode}), respawning immediately`);
|
|
141
|
+
return spawnServer(serverArgs, logFd);
|
|
142
|
+
}
|
|
143
|
+
|
|
138
144
|
// SIGUSR2 restart โ skip backoff, respawn immediately
|
|
139
145
|
if (serverRestartRequested) {
|
|
140
146
|
serverRestartRequested = false;
|