feat(mac): sessions submenus
This commit is contained in:
@@ -73,8 +73,14 @@ import {
|
||||
SendParamsSchema,
|
||||
type SessionsListParams,
|
||||
SessionsListParamsSchema,
|
||||
type SessionsCompactParams,
|
||||
SessionsCompactParamsSchema,
|
||||
type SessionsDeleteParams,
|
||||
SessionsDeleteParamsSchema,
|
||||
type SessionsPatchParams,
|
||||
SessionsPatchParamsSchema,
|
||||
type SessionsResetParams,
|
||||
SessionsResetParamsSchema,
|
||||
type ShutdownEvent,
|
||||
ShutdownEventSchema,
|
||||
type SkillsInstallParams,
|
||||
@@ -143,6 +149,15 @@ export const validateSessionsListParams = ajv.compile<SessionsListParams>(
|
||||
export const validateSessionsPatchParams = ajv.compile<SessionsPatchParams>(
|
||||
SessionsPatchParamsSchema,
|
||||
);
|
||||
export const validateSessionsResetParams = ajv.compile<SessionsResetParams>(
|
||||
SessionsResetParamsSchema,
|
||||
);
|
||||
export const validateSessionsDeleteParams = ajv.compile<SessionsDeleteParams>(
|
||||
SessionsDeleteParamsSchema,
|
||||
);
|
||||
export const validateSessionsCompactParams = ajv.compile<SessionsCompactParams>(
|
||||
SessionsCompactParamsSchema,
|
||||
);
|
||||
export const validateConfigGetParams = ajv.compile<ConfigGetParams>(
|
||||
ConfigGetParamsSchema,
|
||||
);
|
||||
@@ -226,6 +241,9 @@ export {
|
||||
NodeInvokeParamsSchema,
|
||||
SessionsListParamsSchema,
|
||||
SessionsPatchParamsSchema,
|
||||
SessionsResetParamsSchema,
|
||||
SessionsDeleteParamsSchema,
|
||||
SessionsCompactParamsSchema,
|
||||
ConfigGetParamsSchema,
|
||||
ConfigSetParamsSchema,
|
||||
ProvidersStatusParamsSchema,
|
||||
@@ -286,6 +304,9 @@ export type {
|
||||
NodeInvokeParams,
|
||||
SessionsListParams,
|
||||
SessionsPatchParams,
|
||||
SessionsResetParams,
|
||||
SessionsDeleteParams,
|
||||
SessionsCompactParams,
|
||||
CronJob,
|
||||
CronListParams,
|
||||
CronStatusParams,
|
||||
|
||||
@@ -291,6 +291,30 @@ export const SessionsPatchParamsSchema = Type.Object(
|
||||
key: NonEmptyString,
|
||||
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
syncing: Type.Optional(
|
||||
Type.Union([Type.Boolean(), NonEmptyString, Type.Null()]),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SessionsResetParamsSchema = Type.Object(
|
||||
{ key: NonEmptyString },
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SessionsDeleteParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
deleteTranscript: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const SessionsCompactParamsSchema = Type.Object(
|
||||
{
|
||||
key: NonEmptyString,
|
||||
maxLines: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
@@ -629,6 +653,9 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||
NodeInvokeParams: NodeInvokeParamsSchema,
|
||||
SessionsListParams: SessionsListParamsSchema,
|
||||
SessionsPatchParams: SessionsPatchParamsSchema,
|
||||
SessionsResetParams: SessionsResetParamsSchema,
|
||||
SessionsDeleteParams: SessionsDeleteParamsSchema,
|
||||
SessionsCompactParams: SessionsCompactParamsSchema,
|
||||
ConfigGetParams: ConfigGetParamsSchema,
|
||||
ConfigSetParams: ConfigSetParamsSchema,
|
||||
ProvidersStatusParams: ProvidersStatusParamsSchema,
|
||||
@@ -681,6 +708,9 @@ export type NodeDescribeParams = Static<typeof NodeDescribeParamsSchema>;
|
||||
export type NodeInvokeParams = Static<typeof NodeInvokeParamsSchema>;
|
||||
export type SessionsListParams = Static<typeof SessionsListParamsSchema>;
|
||||
export type SessionsPatchParams = Static<typeof SessionsPatchParamsSchema>;
|
||||
export type SessionsResetParams = Static<typeof SessionsResetParamsSchema>;
|
||||
export type SessionsDeleteParams = Static<typeof SessionsDeleteParamsSchema>;
|
||||
export type SessionsCompactParams = Static<typeof SessionsCompactParamsSchema>;
|
||||
export type ConfigGetParams = Static<typeof ConfigGetParamsSchema>;
|
||||
export type ConfigSetParams = Static<typeof ConfigSetParamsSchema>;
|
||||
export type ProvidersStatusParams = Static<typeof ProvidersStatusParamsSchema>;
|
||||
|
||||
@@ -3388,6 +3388,19 @@ describe("gateway server", () => {
|
||||
const now = Date.now();
|
||||
testSessionStorePath = storePath;
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-main.jsonl"),
|
||||
Array.from({ length: 10 })
|
||||
.map((_, idx) => JSON.stringify({ role: "user", content: `line ${idx}` }))
|
||||
.join("\n") + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-group.jsonl"),
|
||||
JSON.stringify({ role: "user", content: "group line 0" }) + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
@@ -3421,7 +3434,15 @@ describe("gateway server", () => {
|
||||
expect(
|
||||
(hello as unknown as { features?: { methods?: string[] } }).features
|
||||
?.methods,
|
||||
).toEqual(expect.arrayContaining(["sessions.list", "sessions.patch"]));
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
"sessions.list",
|
||||
"sessions.patch",
|
||||
"sessions.reset",
|
||||
"sessions.delete",
|
||||
"sessions.compact",
|
||||
]),
|
||||
);
|
||||
|
||||
const list1 = await rpcReq<{
|
||||
path: string;
|
||||
@@ -3483,6 +3504,63 @@ describe("gateway server", () => {
|
||||
expect(main2?.thinkingLevel).toBe("medium");
|
||||
expect(main2?.verboseLevel).toBeUndefined();
|
||||
|
||||
const syncPatched = await rpcReq<{ ok: true; key: string }>(
|
||||
ws,
|
||||
"sessions.patch",
|
||||
{ key: "main", syncing: true },
|
||||
);
|
||||
expect(syncPatched.ok).toBe(true);
|
||||
|
||||
const list3 = await rpcReq<{
|
||||
sessions: Array<{ key: string; syncing?: boolean | string }>;
|
||||
}>(ws, "sessions.list", {});
|
||||
expect(list3.ok).toBe(true);
|
||||
const main3 = list3.payload?.sessions.find((s) => s.key === "main");
|
||||
expect(main3?.syncing).toBe(true);
|
||||
|
||||
const compacted = await rpcReq<{ ok: true; compacted: boolean }>(
|
||||
ws,
|
||||
"sessions.compact",
|
||||
{ key: "main", maxLines: 3 },
|
||||
);
|
||||
expect(compacted.ok).toBe(true);
|
||||
expect(compacted.payload?.compacted).toBe(true);
|
||||
const compactedLines = (
|
||||
await fs.readFile(path.join(dir, "sess-main.jsonl"), "utf-8")
|
||||
)
|
||||
.split(/\r?\n/)
|
||||
.filter((l) => l.trim().length > 0);
|
||||
expect(compactedLines).toHaveLength(3);
|
||||
const filesAfterCompact = await fs.readdir(dir);
|
||||
expect(filesAfterCompact.some((f) => f.startsWith("sess-main.jsonl.bak.")))
|
||||
.toBe(true);
|
||||
|
||||
const deleted = await rpcReq<{ ok: true; deleted: boolean }>(
|
||||
ws,
|
||||
"sessions.delete",
|
||||
{ key: "group:dev" },
|
||||
);
|
||||
expect(deleted.ok).toBe(true);
|
||||
expect(deleted.payload?.deleted).toBe(true);
|
||||
const listAfterDelete = await rpcReq<{
|
||||
sessions: Array<{ key: string }>;
|
||||
}>(ws, "sessions.list", {});
|
||||
expect(listAfterDelete.ok).toBe(true);
|
||||
expect(listAfterDelete.payload?.sessions.some((s) => s.key === "group:dev"))
|
||||
.toBe(false);
|
||||
const filesAfterDelete = await fs.readdir(dir);
|
||||
expect(filesAfterDelete.some((f) => f.startsWith("sess-group.jsonl.deleted.")))
|
||||
.toBe(true);
|
||||
|
||||
const reset = await rpcReq<{ ok: true; key: string; entry: { sessionId: string } }>(
|
||||
ws,
|
||||
"sessions.reset",
|
||||
{ key: "main" },
|
||||
);
|
||||
expect(reset.ok).toBe(true);
|
||||
expect(reset.payload?.key).toBe("main");
|
||||
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
|
||||
|
||||
const badThinking = await rpcReq(ws, "sessions.patch", {
|
||||
key: "main",
|
||||
thinkingLevel: "banana",
|
||||
|
||||
@@ -271,7 +271,10 @@ import {
|
||||
PROTOCOL_VERSION,
|
||||
type RequestFrame,
|
||||
type SessionsListParams,
|
||||
type SessionsCompactParams,
|
||||
type SessionsDeleteParams,
|
||||
type SessionsPatchParams,
|
||||
type SessionsResetParams,
|
||||
type Snapshot,
|
||||
validateAgentParams,
|
||||
validateChatAbortParams,
|
||||
@@ -300,7 +303,10 @@ import {
|
||||
validateRequestFrame,
|
||||
validateSendParams,
|
||||
validateSessionsListParams,
|
||||
validateSessionsCompactParams,
|
||||
validateSessionsDeleteParams,
|
||||
validateSessionsPatchParams,
|
||||
validateSessionsResetParams,
|
||||
validateSkillsInstallParams,
|
||||
validateSkillsStatusParams,
|
||||
validateSkillsUpdateParams,
|
||||
@@ -389,6 +395,9 @@ const METHODS = [
|
||||
"voicewake.set",
|
||||
"sessions.list",
|
||||
"sessions.patch",
|
||||
"sessions.reset",
|
||||
"sessions.delete",
|
||||
"sessions.compact",
|
||||
"last-heartbeat",
|
||||
"set-heartbeats",
|
||||
"wake",
|
||||
@@ -697,27 +706,7 @@ function readSessionMessages(
|
||||
sessionId: string,
|
||||
storePath: string | undefined,
|
||||
): unknown[] {
|
||||
const candidates: string[] = [];
|
||||
if (storePath) {
|
||||
const dir = path.dirname(storePath);
|
||||
candidates.push(path.join(dir, `${sessionId}.jsonl`));
|
||||
}
|
||||
candidates.push(
|
||||
path.join(os.homedir(), ".clawdis", "sessions", `${sessionId}.jsonl`),
|
||||
);
|
||||
candidates.push(
|
||||
path.join(os.homedir(), ".pi", "agent", "sessions", `${sessionId}.jsonl`),
|
||||
);
|
||||
candidates.push(
|
||||
path.join(
|
||||
os.homedir(),
|
||||
".tau",
|
||||
"agent",
|
||||
"sessions",
|
||||
"clawdis",
|
||||
`${sessionId}.jsonl`,
|
||||
),
|
||||
);
|
||||
const candidates = resolveSessionTranscriptCandidates(sessionId, storePath);
|
||||
|
||||
const filePath = candidates.find((p) => fs.existsSync(p));
|
||||
if (!filePath) return [];
|
||||
@@ -741,6 +730,41 @@ function readSessionMessages(
|
||||
return messages;
|
||||
}
|
||||
|
||||
function resolveSessionTranscriptCandidates(
|
||||
sessionId: string,
|
||||
storePath: string | undefined,
|
||||
): string[] {
|
||||
const candidates: string[] = [];
|
||||
if (storePath) {
|
||||
const dir = path.dirname(storePath);
|
||||
candidates.push(path.join(dir, `${sessionId}.jsonl`));
|
||||
}
|
||||
candidates.push(
|
||||
path.join(os.homedir(), ".clawdis", "sessions", `${sessionId}.jsonl`),
|
||||
);
|
||||
candidates.push(
|
||||
path.join(os.homedir(), ".pi", "agent", "sessions", `${sessionId}.jsonl`),
|
||||
);
|
||||
candidates.push(
|
||||
path.join(
|
||||
os.homedir(),
|
||||
".tau",
|
||||
"agent",
|
||||
"sessions",
|
||||
"clawdis",
|
||||
`${sessionId}.jsonl`,
|
||||
),
|
||||
);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function archiveFileOnDisk(filePath: string, reason: string): string {
|
||||
const ts = new Date().toISOString().replaceAll(":", "-");
|
||||
const archived = `${filePath}.${reason}.${ts}`;
|
||||
fs.renameSync(filePath, archived);
|
||||
return archived;
|
||||
}
|
||||
|
||||
function jsonUtf8Bytes(value: unknown): number {
|
||||
try {
|
||||
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
||||
@@ -1991,6 +2015,206 @@ export async function startGatewayServer(
|
||||
};
|
||||
return { ok: true, payloadJSON: JSON.stringify(payload) };
|
||||
}
|
||||
case "sessions.reset": {
|
||||
const params = parseParams();
|
||||
if (!validateSessionsResetParams(params)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: `invalid sessions.reset params: ${formatValidationErrors(validateSessionsResetParams.errors)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const p = params as SessionsResetParams;
|
||||
const key = String(p.key ?? "").trim();
|
||||
if (!key) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "key required",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { storePath, store, entry } = loadSessionEntry(key);
|
||||
const now = Date.now();
|
||||
const next: SessionEntry = {
|
||||
sessionId: randomUUID(),
|
||||
updatedAt: now,
|
||||
systemSent: false,
|
||||
abortedLastRun: false,
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
syncing: entry?.syncing,
|
||||
model: entry?.model,
|
||||
contextTokens: entry?.contextTokens,
|
||||
lastChannel: entry?.lastChannel,
|
||||
lastTo: entry?.lastTo,
|
||||
skillsSnapshot: entry?.skillsSnapshot,
|
||||
};
|
||||
store[key] = next;
|
||||
await saveSessionStore(storePath, store);
|
||||
return {
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({ ok: true, key, entry: next }),
|
||||
};
|
||||
}
|
||||
case "sessions.delete": {
|
||||
const params = parseParams();
|
||||
if (!validateSessionsDeleteParams(params)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: `invalid sessions.delete params: ${formatValidationErrors(validateSessionsDeleteParams.errors)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const p = params as SessionsDeleteParams;
|
||||
const key = String(p.key ?? "").trim();
|
||||
if (!key) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "key required",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const deleteTranscript =
|
||||
typeof p.deleteTranscript === "boolean" ? p.deleteTranscript : true;
|
||||
|
||||
const { storePath, store, entry } = loadSessionEntry(key);
|
||||
const sessionId = entry?.sessionId;
|
||||
const existed = Boolean(store[key]);
|
||||
if (existed) delete store[key];
|
||||
await saveSessionStore(storePath, store);
|
||||
|
||||
const archived: string[] = [];
|
||||
if (deleteTranscript && sessionId) {
|
||||
for (const candidate of resolveSessionTranscriptCandidates(
|
||||
sessionId,
|
||||
storePath,
|
||||
)) {
|
||||
if (!fs.existsSync(candidate)) continue;
|
||||
try {
|
||||
archived.push(archiveFileOnDisk(candidate, "deleted"));
|
||||
} catch {
|
||||
// Best-effort; deleting the store entry is the main operation.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({
|
||||
ok: true,
|
||||
key,
|
||||
deleted: existed,
|
||||
archived,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case "sessions.compact": {
|
||||
const params = parseParams();
|
||||
if (!validateSessionsCompactParams(params)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: `invalid sessions.compact params: ${formatValidationErrors(validateSessionsCompactParams.errors)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const p = params as SessionsCompactParams;
|
||||
const key = String(p.key ?? "").trim();
|
||||
if (!key) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "key required",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const maxLines =
|
||||
typeof p.maxLines === "number" && Number.isFinite(p.maxLines)
|
||||
? Math.max(1, Math.floor(p.maxLines))
|
||||
: 400;
|
||||
|
||||
const { storePath, store, entry } = loadSessionEntry(key);
|
||||
const sessionId = entry?.sessionId;
|
||||
if (!sessionId) {
|
||||
return {
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({
|
||||
ok: true,
|
||||
key,
|
||||
compacted: false,
|
||||
reason: "no sessionId",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const filePath = resolveSessionTranscriptCandidates(sessionId, storePath)
|
||||
.find((candidate) => fs.existsSync(candidate));
|
||||
if (!filePath) {
|
||||
return {
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({
|
||||
ok: true,
|
||||
key,
|
||||
compacted: false,
|
||||
reason: "no transcript",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
||||
if (lines.length <= maxLines) {
|
||||
return {
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({
|
||||
ok: true,
|
||||
key,
|
||||
compacted: false,
|
||||
kept: lines.length,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const archived = archiveFileOnDisk(filePath, "bak");
|
||||
const keptLines = lines.slice(-maxLines);
|
||||
fs.writeFileSync(filePath, `${keptLines.join("\n")}\n`, "utf-8");
|
||||
|
||||
// Token counts no longer match; clear so status + UI reflect reality after the next turn.
|
||||
if (store[key]) {
|
||||
delete store[key].inputTokens;
|
||||
delete store[key].outputTokens;
|
||||
delete store[key].totalTokens;
|
||||
store[key].updatedAt = Date.now();
|
||||
await saveSessionStore(storePath, store);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({
|
||||
ok: true,
|
||||
key,
|
||||
compacted: true,
|
||||
archived,
|
||||
kept: keptLines.length,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case "chat.history": {
|
||||
const params = parseParams();
|
||||
if (!validateChatHistoryParams(params)) {
|
||||
@@ -4056,6 +4280,15 @@ export async function startGatewayServer(
|
||||
}
|
||||
}
|
||||
|
||||
if ("syncing" in p) {
|
||||
const raw = p.syncing;
|
||||
if (raw === null) {
|
||||
delete next.syncing;
|
||||
} else if (raw !== undefined) {
|
||||
next.syncing = raw as boolean | string;
|
||||
}
|
||||
}
|
||||
|
||||
store[key] = next;
|
||||
await saveSessionStore(storePath, store);
|
||||
const result: SessionsPatchResult = {
|
||||
@@ -4067,6 +4300,199 @@ export async function startGatewayServer(
|
||||
respond(true, result, undefined);
|
||||
break;
|
||||
}
|
||||
case "sessions.reset": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSessionsResetParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid sessions.reset params: ${formatValidationErrors(validateSessionsResetParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const p = params as SessionsResetParams;
|
||||
const key = String(p.key ?? "").trim();
|
||||
if (!key) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "key required"),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const { storePath, store, entry } = loadSessionEntry(key);
|
||||
const now = Date.now();
|
||||
const next: SessionEntry = {
|
||||
sessionId: randomUUID(),
|
||||
updatedAt: now,
|
||||
systemSent: false,
|
||||
abortedLastRun: false,
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
syncing: entry?.syncing,
|
||||
model: entry?.model,
|
||||
contextTokens: entry?.contextTokens,
|
||||
lastChannel: entry?.lastChannel,
|
||||
lastTo: entry?.lastTo,
|
||||
skillsSnapshot: entry?.skillsSnapshot,
|
||||
};
|
||||
store[key] = next;
|
||||
await saveSessionStore(storePath, store);
|
||||
respond(true, { ok: true, key, entry: next }, undefined);
|
||||
break;
|
||||
}
|
||||
case "sessions.delete": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSessionsDeleteParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid sessions.delete params: ${formatValidationErrors(validateSessionsDeleteParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const p = params as SessionsDeleteParams;
|
||||
const key = String(p.key ?? "").trim();
|
||||
if (!key) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "key required"),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const deleteTranscript =
|
||||
typeof p.deleteTranscript === "boolean"
|
||||
? p.deleteTranscript
|
||||
: true;
|
||||
|
||||
const { storePath, store, entry } = loadSessionEntry(key);
|
||||
const sessionId = entry?.sessionId;
|
||||
const existed = Boolean(store[key]);
|
||||
if (existed) delete store[key];
|
||||
await saveSessionStore(storePath, store);
|
||||
|
||||
const archived: string[] = [];
|
||||
if (deleteTranscript && sessionId) {
|
||||
for (const candidate of resolveSessionTranscriptCandidates(
|
||||
sessionId,
|
||||
storePath,
|
||||
)) {
|
||||
if (!fs.existsSync(candidate)) continue;
|
||||
try {
|
||||
archived.push(archiveFileOnDisk(candidate, "deleted"));
|
||||
} catch {
|
||||
// Best-effort.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respond(
|
||||
true,
|
||||
{ ok: true, key, deleted: existed, archived },
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "sessions.compact": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSessionsCompactParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid sessions.compact params: ${formatValidationErrors(validateSessionsCompactParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const p = params as SessionsCompactParams;
|
||||
const key = String(p.key ?? "").trim();
|
||||
if (!key) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "key required"),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const maxLines =
|
||||
typeof p.maxLines === "number" && Number.isFinite(p.maxLines)
|
||||
? Math.max(1, Math.floor(p.maxLines))
|
||||
: 400;
|
||||
|
||||
const { storePath, store, entry } = loadSessionEntry(key);
|
||||
const sessionId = entry?.sessionId;
|
||||
if (!sessionId) {
|
||||
respond(
|
||||
true,
|
||||
{ ok: true, key, compacted: false, reason: "no sessionId" },
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const filePath = resolveSessionTranscriptCandidates(
|
||||
sessionId,
|
||||
storePath,
|
||||
).find((candidate) => fs.existsSync(candidate));
|
||||
if (!filePath) {
|
||||
respond(
|
||||
true,
|
||||
{ ok: true, key, compacted: false, reason: "no transcript" },
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const lines = raw
|
||||
.split(/\r?\n/)
|
||||
.filter((l) => l.trim().length > 0);
|
||||
if (lines.length <= maxLines) {
|
||||
respond(
|
||||
true,
|
||||
{ ok: true, key, compacted: false, kept: lines.length },
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const archived = archiveFileOnDisk(filePath, "bak");
|
||||
const keptLines = lines.slice(-maxLines);
|
||||
fs.writeFileSync(filePath, `${keptLines.join("\n")}\n`, "utf-8");
|
||||
|
||||
if (store[key]) {
|
||||
delete store[key].inputTokens;
|
||||
delete store[key].outputTokens;
|
||||
delete store[key].totalTokens;
|
||||
store[key].updatedAt = Date.now();
|
||||
await saveSessionStore(storePath, store);
|
||||
}
|
||||
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
key,
|
||||
compacted: true,
|
||||
archived,
|
||||
kept: keptLines.length,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "last-heartbeat": {
|
||||
respond(true, getLastHeartbeatEvent(), undefined);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user