chatccc 0.2.45 → 0.2.47

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.
@@ -0,0 +1,94 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+
4
+ import { USER_DATA_DIR, ts } from "./config.ts";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // stream-state.json — 每个 session 的流式输出持久化文件
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export const STREAMS_DIR = join(USER_DATA_DIR, "state", "streams");
11
+
12
+ export interface StreamState {
13
+ sessionId: string;
14
+ status: "running" | "done" | "stopped" | "error";
15
+ accumulatedContent: string;
16
+ finalReply: string;
17
+ chunkCount: number;
18
+ turnCount: number;
19
+ contextTokens: number;
20
+ updatedAt: number;
21
+ cwd: string;
22
+ tool: string;
23
+ }
24
+
25
+ function getStreamStatePath(sessionId: string): string {
26
+ return join(STREAMS_DIR, `${sessionId}.json`);
27
+ }
28
+
29
+ export async function readStreamState(sessionId: string): Promise<StreamState | null> {
30
+ try {
31
+ const raw = await readFile(getStreamStatePath(sessionId), "utf-8");
32
+ const parsed = JSON.parse(raw) as StreamState;
33
+ if (parsed && typeof parsed.sessionId === "string") return parsed;
34
+ return null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export async function writeStreamState(state: StreamState): Promise<void> {
41
+ const filePath = getStreamStatePath(state.sessionId);
42
+ try {
43
+ await mkdir(dirname(filePath), { recursive: true });
44
+ // 先写临时文件再 rename,保证原子性
45
+ const tmpPath = filePath + ".tmp";
46
+ await writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
47
+ await writeFile(filePath, JSON.stringify(state, null, 2), "utf-8");
48
+ } catch (err) {
49
+ console.error(`[${ts()}] Failed to write stream-state for ${state.sessionId}: ${(err as Error).message}`);
50
+ }
51
+ }
52
+
53
+ export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
54
+ return {
55
+ sessionId,
56
+ status: "running",
57
+ accumulatedContent: "",
58
+ finalReply: "",
59
+ chunkCount: 0,
60
+ turnCount,
61
+ contextTokens: 0,
62
+ updatedAt: Date.now(),
63
+ cwd,
64
+ tool,
65
+ };
66
+ }
67
+
68
+ /** 进程重启时修正虚假的 "running" 状态 */
69
+ export async function fixStaleStreamStates(): Promise<void> {
70
+ // 启动时将残留的 running 标记为 error
71
+ // 实际 agent 进程已不存在,下次 prompt 时会自然覆盖
72
+ try {
73
+ const { readdir } = await import("node:fs/promises");
74
+ let entries: string[];
75
+ try {
76
+ entries = await readdir(STREAMS_DIR);
77
+ } catch {
78
+ return;
79
+ }
80
+ for (const entry of entries) {
81
+ if (!entry.endsWith(".json")) continue;
82
+ const state = await readStreamState(entry.replace(".json", ""));
83
+ if (state && state.status === "running") {
84
+ state.status = "error";
85
+ state.accumulatedContent += "\n\n⚠️ 进程重启,流式输出中断。";
86
+ state.updatedAt = Date.now();
87
+ await writeStreamState(state);
88
+ console.log(`[${ts()}] [STREAM-STATE] marked stale running as error: ${state.sessionId}`);
89
+ }
90
+ }
91
+ } catch (err) {
92
+ console.error(`[${ts()}] [STREAM-STATE] fixStaleStreamStates failed: ${(err as Error).message}`);
93
+ }
94
+ }
@@ -1,71 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
-
3
- import type { AgentImageGrant } from "./agent-image-rpc.ts";
4
- import type { AgentFileGrant } from "./agent-file-rpc.ts";
5
- import { ts } from "./config.ts";
6
-
7
- export const AGENT_SESSION_GRANTS_PATH = "/api/agent/session-grants";
8
-
9
- interface SessionGrantsEntry {
10
- image: AgentImageGrant;
11
- file: AgentFileGrant;
12
- }
13
-
14
- const sessionGrants = new Map<string, SessionGrantsEntry>();
15
-
16
- export function setSessionGrants(
17
- sessionId: string,
18
- imageGrant: AgentImageGrant,
19
- fileGrant: AgentFileGrant,
20
- ): void {
21
- sessionGrants.set(sessionId, { image: imageGrant, file: fileGrant });
22
- }
23
-
24
- export function clearSessionGrants(sessionId: string): void {
25
- sessionGrants.delete(sessionId);
26
- }
27
-
28
- function jsonReply(res: ServerResponse, status: number, data: unknown): void {
29
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
30
- res.end(JSON.stringify(data));
31
- }
32
-
33
- export async function handleAgentGrantsRequest(
34
- req: IncomingMessage,
35
- res: ServerResponse,
36
- ): Promise<boolean> {
37
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
38
- if (url.pathname !== AGENT_SESSION_GRANTS_PATH) return false;
39
-
40
- if (req.method !== "GET") {
41
- jsonReply(res, 405, { ok: false, error: "Method not allowed" });
42
- return true;
43
- }
44
-
45
- const sid = url.searchParams.get("sid");
46
- if (!sid) {
47
- jsonReply(res, 400, { ok: false, error: "Missing sid parameter" });
48
- return true;
49
- }
50
-
51
- const entry = sessionGrants.get(sid);
52
- if (!entry) {
53
- jsonReply(res, 404, { ok: false, error: "No active grants for this session" });
54
- return true;
55
- }
56
-
57
- const now = Date.now();
58
- if (entry.image.expiresAt <= now || entry.file.expiresAt <= now) {
59
- sessionGrants.delete(sid);
60
- jsonReply(res, 410, { ok: false, error: "Session grants expired" });
61
- return true;
62
- }
63
-
64
- console.log(`[${ts()}] [AGENT-GRANTS] query session=${sid}`);
65
- jsonReply(res, 200, {
66
- ok: true,
67
- image: { url: entry.image.url, token: entry.image.token },
68
- file: { url: entry.file.url, token: entry.file.token },
69
- });
70
- return true;
71
- }