chatccc 0.2.41 → 0.2.42

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/src/session.ts CHANGED
@@ -168,6 +168,87 @@ export async function getSessionTool(sessionId: string): Promise<string | null>
168
168
  return record?.tool ?? null;
169
169
  }
170
170
 
171
+ // ---------------------------------------------------------------------------
172
+ // Conversation session registry for /sessions
173
+ // ---------------------------------------------------------------------------
174
+
175
+ export const SESSION_REGISTRY_FILE = join(USER_DATA_DIR, "state", "session-registry.json");
176
+ let sessionRegistryFile = SESSION_REGISTRY_FILE;
177
+
178
+ export interface SessionRegistryUpdate {
179
+ chatId: string;
180
+ sessionId: string;
181
+ tool: string;
182
+ chatName?: string;
183
+ turnCount?: number;
184
+ lastContextTokens?: number;
185
+ startTime?: number;
186
+ updatedAt?: number;
187
+ running?: boolean;
188
+ }
189
+
190
+ interface SessionRegistryRecord {
191
+ chatId: string;
192
+ sessionId: string;
193
+ tool: string;
194
+ chatName: string;
195
+ turnCount: number;
196
+ lastContextTokens: number;
197
+ startTime: number;
198
+ updatedAt: number;
199
+ running: boolean;
200
+ }
201
+
202
+ type SessionRegistryData = Record<string, SessionRegistryRecord>;
203
+
204
+ async function loadSessionRegistry(): Promise<SessionRegistryData> {
205
+ try {
206
+ const raw = await readFile(sessionRegistryFile, "utf-8");
207
+ const parsed = JSON.parse(raw) as SessionRegistryData;
208
+ return parsed && typeof parsed === "object" ? parsed : {};
209
+ } catch {
210
+ return {};
211
+ }
212
+ }
213
+
214
+ async function saveSessionRegistry(data: SessionRegistryData): Promise<void> {
215
+ try {
216
+ await mkdir(dirname(sessionRegistryFile), { recursive: true });
217
+ await writeFile(sessionRegistryFile, JSON.stringify(data, null, 2), "utf-8");
218
+ } catch (err) {
219
+ console.error(`[${ts()}] Failed to save session-registry.json: ${(err as Error).message}`);
220
+ fileLog.flush();
221
+ }
222
+ }
223
+
224
+ export async function recordSessionRegistry(update: SessionRegistryUpdate): Promise<void> {
225
+ const data = await loadSessionRegistry();
226
+ const existing = data[update.chatId];
227
+ const now = update.updatedAt ?? Date.now();
228
+
229
+ data[update.chatId] = {
230
+ chatId: update.chatId,
231
+ sessionId: update.sessionId,
232
+ tool: update.tool,
233
+ chatName: update.chatName ?? existing?.chatName ?? "",
234
+ turnCount: update.turnCount ?? existing?.turnCount ?? 0,
235
+ lastContextTokens: update.lastContextTokens ?? existing?.lastContextTokens ?? 0,
236
+ startTime: update.startTime ?? existing?.startTime ?? now,
237
+ updatedAt: now,
238
+ running: update.running ?? existing?.running ?? false,
239
+ };
240
+
241
+ await saveSessionRegistry(data);
242
+ }
243
+
244
+ export function _setSessionRegistryFileForTest(filePath: string): void {
245
+ sessionRegistryFile = filePath;
246
+ }
247
+
248
+ export function _resetSessionRegistryFileForTest(): void {
249
+ sessionRegistryFile = SESSION_REGISTRY_FILE;
250
+ }
251
+
171
252
  // ---------------------------------------------------------------------------
172
253
  // accumulateBlockContent — 将 UnifiedBlock 累积到渲染状态(纯函数,可测试)
173
254
  // ---------------------------------------------------------------------------
@@ -385,13 +466,24 @@ export async function resumeAndPrompt(
385
466
 
386
467
  const now = Date.now();
387
468
  const existingInfo = sessionInfoMap.get(chatId);
469
+ const nextTurnCount = (existingInfo?.turnCount ?? 0) + 1;
470
+ const nextContextTokens = existingInfo?.lastContextTokens ?? 0;
388
471
  sessionInfoMap.set(chatId, {
389
472
  sessionId,
390
- turnCount: (existingInfo?.turnCount ?? 0) + 1,
391
- lastContextTokens: existingInfo?.lastContextTokens ?? 0,
473
+ turnCount: nextTurnCount,
474
+ lastContextTokens: nextContextTokens,
392
475
  startTime: now,
393
476
  tool,
394
477
  });
478
+ await recordSessionRegistry({
479
+ chatId,
480
+ sessionId,
481
+ tool,
482
+ turnCount: nextTurnCount,
483
+ lastContextTokens: nextContextTokens,
484
+ startTime: now,
485
+ running: true,
486
+ });
395
487
 
396
488
  let cardId: string | null = null;
397
489
  cardId = await createCardKitCard(token, buildProgressCard("", { showStop: true, headerTitle: "生成中..." })).catch((err) => {
@@ -512,6 +604,13 @@ export async function resumeAndPrompt(
512
604
  if (block.type === "compact_boundary" && block.post_tokens) {
513
605
  const info = sessionInfoMap.get(chatId);
514
606
  if (info) { info.lastContextTokens = block.post_tokens; }
607
+ await recordSessionRegistry({
608
+ chatId,
609
+ sessionId,
610
+ tool,
611
+ lastContextTokens: block.post_tokens,
612
+ running: true,
613
+ });
515
614
  }
516
615
  }
517
616
  }
@@ -561,6 +660,16 @@ export async function resumeAndPrompt(
561
660
  const finalReply = pickFinalReply(state).trim();
562
661
 
563
662
  if (wasStopped) {
663
+ const finalInfo = sessionInfoMap.get(chatId);
664
+ await recordSessionRegistry({
665
+ chatId,
666
+ sessionId,
667
+ tool,
668
+ turnCount: finalInfo?.turnCount ?? nextTurnCount,
669
+ lastContextTokens: finalInfo?.lastContextTokens ?? nextContextTokens,
670
+ startTime: finalInfo?.startTime ?? now,
671
+ running: false,
672
+ });
564
673
  if (finalReply) {
565
674
  await sendTextReply(token, chatId, finalReply).catch((err) =>
566
675
  console.error(`[${ts()}] Failed to send partial text: ${(err as Error).message}`)
@@ -597,6 +706,16 @@ export async function resumeAndPrompt(
597
706
  }
598
707
 
599
708
  console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
709
+ const finalInfo = sessionInfoMap.get(chatId);
710
+ await recordSessionRegistry({
711
+ chatId,
712
+ sessionId,
713
+ tool,
714
+ turnCount: finalInfo?.turnCount ?? nextTurnCount,
715
+ lastContextTokens: finalInfo?.lastContextTokens ?? nextContextTokens,
716
+ startTime: finalInfo?.startTime ?? now,
717
+ running: false,
718
+ });
600
719
  if (tid) logTrace(tid, "SESSION_END", { sessionId, chunks: state.chunkCount, finalTextLen: finalReply.length });
601
720
  }
602
721
 
@@ -681,6 +800,7 @@ export async function getSessionStatus(chatId: string): Promise<SessionStatus |
681
800
  export interface SessionsListEntry {
682
801
  chatId: string;
683
802
  sessionId: string;
803
+ chatName: string;
684
804
  active: boolean;
685
805
  turnCount: number;
686
806
  startTime: number;
@@ -691,16 +811,20 @@ export interface SessionsListEntry {
691
811
  }
692
812
 
693
813
  export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
694
- const entries = Array.from(sessionInfoMap.entries());
814
+ const registry = await loadSessionRegistry();
815
+ const entries = Object.values(registry)
816
+ .filter((record) => record.chatId && record.sessionId && record.tool)
817
+ .sort((a, b) => b.updatedAt - a.updatedAt)
818
+ .slice(0, 20);
695
819
  // 并行解析每个 session 的 model/effort(cursor 涉及异步 store IO)
696
820
  return Promise.all(
697
- entries.map(async ([chatId, info]) => {
698
- const active = chatSessionMap.get(chatId);
821
+ entries.map(async (info) => {
699
822
  const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
700
823
  return {
701
- chatId,
824
+ chatId: info.chatId,
702
825
  sessionId: info.sessionId,
703
- active: active !== undefined && !active.stopped,
826
+ chatName: info.chatName || "",
827
+ active: info.running === true,
704
828
  turnCount: info.turnCount,
705
829
  startTime: info.startTime,
706
830
  model,
package/src/shared.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import { execSync } from "node:child_process";
2
- import {
3
- appendFileSync,
4
- existsSync,
5
- mkdirSync,
6
- readFileSync,
7
- unlinkSync,
8
- writeFileSync,
9
- } from "node:fs";
10
- import { createServer } from "node:http";
11
- import { homedir } from "node:os";
12
- import { join } from "node:path";
13
- import { inspect } from "node:util";
14
- import { WebSocketServer, WebSocket } from "ws";
2
+ import {
3
+ appendFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ unlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { createServer } from "node:http";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { inspect } from "node:util";
14
+ import { WebSocketServer, WebSocket } from "ws";
15
15
 
16
16
  import { printServiceDidNotStart } from "./exit-banner.ts";
17
17
 
@@ -355,56 +355,56 @@ export function installCrashLogging(opts: CrashHandlersOptions = {}): InstallCra
355
355
  // 文件日志:同时输出到控制台和日志文件
356
356
  // ---------------------------------------------------------------------------
357
357
 
358
- export function setupFileLogging(logDir: string, prefix: string): { logPath: string; flush: () => void } {
359
- mkdirSync(logDir, { recursive: true });
360
- const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
361
- const logPath = join(logDir, `${prefix}-${ts}.log`);
362
- writeFileSync(logPath, "", { flag: "a", encoding: "utf8" });
363
- const origConsoleLog = console.log.bind(console);
364
- const origConsoleError = console.error.bind(console);
365
- const formatArg = (arg: unknown): string => {
366
- if (typeof arg === "string") return arg;
367
- if (arg instanceof Error) return arg.stack ?? arg.message;
368
- return inspect(arg, { depth: 6, breakLength: Infinity, maxArrayLength: 200 });
369
- };
370
- const writeLine = (level: string, args: unknown[]) => {
371
- try {
372
- const line = args.map(formatArg).join(" ");
373
- appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${line}\n`, "utf8");
374
- } catch (err) {
375
- try {
376
- appendStartupTrace("fileLog write failed", { level, error: err instanceof Error ? err.message : String(err) });
377
- } catch {
378
- // 日志系统自身不能影响主流程
379
- }
380
- }
381
- };
382
- console.log = (...args: unknown[]) => {
383
- writeLine("LOG", args);
384
- try {
385
- origConsoleLog(...args);
386
- } catch {
387
- // 控制台输出失败也不能拖垮服务
388
- }
389
- };
390
- console.error = (...args: unknown[]) => {
391
- writeLine("ERR", args);
392
- try {
393
- origConsoleError(...args);
394
- } catch {
395
- // 控制台输出失败也不能拖垮服务
396
- }
397
- };
398
- const flush = () => {
399
- try {
400
- appendFileSync(logPath, "", "utf8");
401
- } catch (err) {
402
- appendStartupTrace("fileLog flush failed", { error: err instanceof Error ? err.message : String(err) });
403
- }
404
- };
405
- origConsoleLog(`Log file: ${logPath}`);
406
- return { logPath, flush };
407
- }
358
+ export function setupFileLogging(logDir: string, prefix: string): { logPath: string; flush: () => void } {
359
+ mkdirSync(logDir, { recursive: true });
360
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
361
+ const logPath = join(logDir, `${prefix}-${ts}.log`);
362
+ writeFileSync(logPath, "", { flag: "a", encoding: "utf8" });
363
+ const origConsoleLog = console.log.bind(console);
364
+ const origConsoleError = console.error.bind(console);
365
+ const formatArg = (arg: unknown): string => {
366
+ if (typeof arg === "string") return arg;
367
+ if (arg instanceof Error) return arg.stack ?? arg.message;
368
+ return inspect(arg, { depth: 6, breakLength: Infinity, maxArrayLength: 200 });
369
+ };
370
+ const writeLine = (level: string, args: unknown[]) => {
371
+ try {
372
+ const line = args.map(formatArg).join(" ");
373
+ appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${line}\n`, "utf8");
374
+ } catch (err) {
375
+ try {
376
+ appendStartupTrace("fileLog write failed", { level, error: err instanceof Error ? err.message : String(err) });
377
+ } catch {
378
+ // 日志系统自身不能影响主流程
379
+ }
380
+ }
381
+ };
382
+ console.log = (...args: unknown[]) => {
383
+ writeLine("LOG", args);
384
+ try {
385
+ origConsoleLog(...args);
386
+ } catch {
387
+ // 控制台输出失败也不能拖垮服务
388
+ }
389
+ };
390
+ console.error = (...args: unknown[]) => {
391
+ writeLine("ERR", args);
392
+ try {
393
+ origConsoleError(...args);
394
+ } catch {
395
+ // 控制台输出失败也不能拖垮服务
396
+ }
397
+ };
398
+ const flush = () => {
399
+ try {
400
+ appendFileSync(logPath, "", "utf8");
401
+ } catch (err) {
402
+ appendStartupTrace("fileLog flush failed", { error: err instanceof Error ? err.message : String(err) });
403
+ }
404
+ };
405
+ origConsoleLog(`Log file: ${logPath}`);
406
+ return { logPath, flush };
407
+ }
408
408
 
409
409
  // ---------------------------------------------------------------------------
410
410
  // 本地 WebSocket 中继服务器(同一端口、多客户端广播)