refactor: harden session store updates
Co-authored-by: Tyler Yust <tyler6204@users.noreply.github.com>
This commit is contained in:
parent
35492f8513
commit
688a0ce439
@ -25,7 +25,7 @@ import {
|
|||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import {
|
import {
|
||||||
formatUsageSummaryLine,
|
formatUsageSummaryLine,
|
||||||
@ -263,7 +263,9 @@ export function createSessionStatusTool(opts?: {
|
|||||||
delete nextEntry.authProfileOverride;
|
delete nextEntry.authProfileOverride;
|
||||||
}
|
}
|
||||||
store[resolved.key] = nextEntry;
|
store[resolved.key] = nextEntry;
|
||||||
await saveSessionStore(storePath, store);
|
await updateSessionStore(storePath, (nextStore) => {
|
||||||
|
nextStore[resolved.key] = nextEntry;
|
||||||
|
});
|
||||||
resolved.entry = nextEntry;
|
resolved.entry = nextEntry;
|
||||||
changedModel = true;
|
changedModel = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||||
@ -90,7 +90,13 @@ export async function tryFastAbortFromMessage(params: {
|
|||||||
entry.abortedLastRun = true;
|
entry.abortedLastRun = true;
|
||||||
entry.updatedAt = Date.now();
|
entry.updatedAt = Date.now();
|
||||||
store[key] = entry;
|
store[key] = entry;
|
||||||
await saveSessionStore(storePath, store);
|
await updateSessionStore(storePath, (nextStore) => {
|
||||||
|
const nextEntry = nextStore[key] ?? entry;
|
||||||
|
if (!nextEntry) return;
|
||||||
|
nextEntry.abortedLastRun = true;
|
||||||
|
nextEntry.updatedAt = Date.now();
|
||||||
|
nextStore[key] = nextEntry;
|
||||||
|
});
|
||||||
} else if (abortKey) {
|
} else if (abortKey) {
|
||||||
setAbortMemory(abortKey, true);
|
setAbortMemory(abortKey, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
resolveSessionTranscriptPath,
|
resolveSessionTranscriptPath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
|
import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
|
||||||
@ -383,6 +383,7 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
params.activeSessionStore &&
|
params.activeSessionStore &&
|
||||||
params.storePath
|
params.storePath
|
||||||
) {
|
) {
|
||||||
|
const sessionKey = params.sessionKey;
|
||||||
const corruptedSessionId = params.getActiveSessionEntry()?.sessionId;
|
const corruptedSessionId = params.getActiveSessionEntry()?.sessionId;
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
`Session history corrupted (Gemini function call ordering). Resetting session: ${params.sessionKey}`,
|
`Session history corrupted (Gemini function call ordering). Resetting session: ${params.sessionKey}`,
|
||||||
@ -399,9 +400,10 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove session entry from store
|
// Remove session entry from store using a fresh, locked snapshot.
|
||||||
delete params.activeSessionStore[params.sessionKey];
|
await updateSessionStore(params.storePath, (store) => {
|
||||||
await saveSessionStore(params.storePath, params.activeSessionStore);
|
delete store[sessionKey];
|
||||||
|
});
|
||||||
} catch (cleanupErr) {
|
} catch (cleanupErr) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
`Failed to reset corrupted session ${params.sessionKey}: ${String(cleanupErr)}`,
|
`Failed to reset corrupted session ${params.sessionKey}: ${String(cleanupErr)}`,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
resolveSessionTranscriptPath,
|
resolveSessionTranscriptPath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
updateSessionStoreEntry,
|
updateSessionStoreEntry,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import type { TypingMode } from "../../config/types.js";
|
import type { TypingMode } from "../../config/types.js";
|
||||||
@ -156,7 +156,9 @@ export async function runReplyAgent(params: {
|
|||||||
activeSessionEntry.updatedAt = Date.now();
|
activeSessionEntry.updatedAt = Date.now();
|
||||||
activeSessionStore[sessionKey] = activeSessionEntry;
|
activeSessionStore[sessionKey] = activeSessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, activeSessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = activeSessionEntry as SessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
typing.cleanup();
|
typing.cleanup();
|
||||||
@ -170,7 +172,9 @@ export async function runReplyAgent(params: {
|
|||||||
activeSessionEntry.updatedAt = Date.now();
|
activeSessionEntry.updatedAt = Date.now();
|
||||||
activeSessionStore[sessionKey] = activeSessionEntry;
|
activeSessionStore[sessionKey] = activeSessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, activeSessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = activeSessionEntry as SessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
typing.cleanup();
|
typing.cleanup();
|
||||||
@ -224,7 +228,9 @@ export async function runReplyAgent(params: {
|
|||||||
nextEntry.sessionFile = nextSessionFile;
|
nextEntry.sessionFile = nextSessionFile;
|
||||||
activeSessionStore[sessionKey] = nextEntry;
|
activeSessionStore[sessionKey] = nextEntry;
|
||||||
try {
|
try {
|
||||||
await saveSessionStore(storePath, activeSessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = nextEntry;
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
`Failed to persist session reset after compaction failure (${sessionKey}): ${String(err)}`,
|
`Failed to persist session reset after compaction failure (${sessionKey}): ${String(err)}`,
|
||||||
@ -280,7 +286,9 @@ export async function runReplyAgent(params: {
|
|||||||
activeSessionEntry.updatedAt = Date.now();
|
activeSessionEntry.updatedAt = Date.now();
|
||||||
activeSessionStore[sessionKey] = activeSessionEntry;
|
activeSessionStore[sessionKey] = activeSessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, activeSessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = activeSessionEntry as SessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { SessionEntry } from "../../config/sessions.js";
|
import type { SessionEntry } from "../../config/sessions.js";
|
||||||
import { saveSessionStore } from "../../config/sessions.js";
|
import { updateSessionStore } from "../../config/sessions.js";
|
||||||
import { setAbortMemory } from "./abort.js";
|
import { setAbortMemory } from "./abort.js";
|
||||||
|
|
||||||
export async function applySessionHints(params: {
|
export async function applySessionHints(params: {
|
||||||
@ -23,7 +23,16 @@ export async function applySessionHints(params: {
|
|||||||
params.sessionEntry.updatedAt = Date.now();
|
params.sessionEntry.updatedAt = Date.now();
|
||||||
params.sessionStore[params.sessionKey] = params.sessionEntry;
|
params.sessionStore[params.sessionKey] = params.sessionEntry;
|
||||||
if (params.storePath) {
|
if (params.storePath) {
|
||||||
await saveSessionStore(params.storePath, params.sessionStore);
|
const sessionKey = params.sessionKey;
|
||||||
|
await updateSessionStore(params.storePath, (store) => {
|
||||||
|
const entry = store[sessionKey] ?? params.sessionEntry;
|
||||||
|
if (!entry) return;
|
||||||
|
store[sessionKey] = {
|
||||||
|
...entry,
|
||||||
|
abortedLastRun: false,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (params.abortKey) {
|
} else if (params.abortKey) {
|
||||||
setAbortMemory(params.abortKey, false);
|
setAbortMemory(params.abortKey, false);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
|
import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js";
|
||||||
import type { SessionEntry } from "../../config/sessions.js";
|
import type { SessionEntry } from "../../config/sessions.js";
|
||||||
import { saveSessionStore } from "../../config/sessions.js";
|
import { updateSessionStore } from "../../config/sessions.js";
|
||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
import { scheduleGatewaySigusr1Restart, triggerClawdbotRestart } from "../../infra/restart.js";
|
import { scheduleGatewaySigusr1Restart, triggerClawdbotRestart } from "../../infra/restart.js";
|
||||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||||
@ -71,7 +71,9 @@ export const handleActivationCommand: CommandHandler = async (params, allowTextC
|
|||||||
params.sessionEntry.updatedAt = Date.now();
|
params.sessionEntry.updatedAt = Date.now();
|
||||||
params.sessionStore[params.sessionKey] = params.sessionEntry;
|
params.sessionStore[params.sessionKey] = params.sessionEntry;
|
||||||
if (params.storePath) {
|
if (params.storePath) {
|
||||||
await saveSessionStore(params.storePath, params.sessionStore);
|
await updateSessionStore(params.storePath, (store) => {
|
||||||
|
store[params.sessionKey] = params.sessionEntry as SessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -107,7 +109,9 @@ export const handleSendPolicyCommand: CommandHandler = async (params, allowTextC
|
|||||||
params.sessionEntry.updatedAt = Date.now();
|
params.sessionEntry.updatedAt = Date.now();
|
||||||
params.sessionStore[params.sessionKey] = params.sessionEntry;
|
params.sessionStore[params.sessionKey] = params.sessionEntry;
|
||||||
if (params.storePath) {
|
if (params.storePath) {
|
||||||
await saveSessionStore(params.storePath, params.sessionStore);
|
await updateSessionStore(params.storePath, (store) => {
|
||||||
|
store[params.sessionKey] = params.sessionEntry as SessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const label =
|
const label =
|
||||||
@ -190,7 +194,9 @@ export const handleStopCommand: CommandHandler = async (params, allowTextCommand
|
|||||||
abortTarget.entry.updatedAt = Date.now();
|
abortTarget.entry.updatedAt = Date.now();
|
||||||
params.sessionStore[abortTarget.key] = abortTarget.entry;
|
params.sessionStore[abortTarget.key] = abortTarget.entry;
|
||||||
if (params.storePath) {
|
if (params.storePath) {
|
||||||
await saveSessionStore(params.storePath, params.sessionStore);
|
await updateSessionStore(params.storePath, (store) => {
|
||||||
|
store[abortTarget.key] = abortTarget.entry as SessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (params.command.abortKey) {
|
} else if (params.command.abortKey) {
|
||||||
setAbortMemory(params.command.abortKey, true);
|
setAbortMemory(params.command.abortKey, true);
|
||||||
@ -215,7 +221,9 @@ export const handleAbortTrigger: CommandHandler = async (params, allowTextComman
|
|||||||
abortTarget.entry.updatedAt = Date.now();
|
abortTarget.entry.updatedAt = Date.now();
|
||||||
params.sessionStore[abortTarget.key] = abortTarget.entry;
|
params.sessionStore[abortTarget.key] = abortTarget.entry;
|
||||||
if (params.storePath) {
|
if (params.storePath) {
|
||||||
await saveSessionStore(params.storePath, params.sessionStore);
|
await updateSessionStore(params.storePath, (store) => {
|
||||||
|
store[abortTarget.key] = abortTarget.entry as SessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (params.command.abortKey) {
|
} else if (params.command.abortKey) {
|
||||||
setAbortMemory(params.command.abortKey, true);
|
setAbortMemory(params.command.abortKey, true);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope
|
|||||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||||
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
||||||
@ -288,7 +288,9 @@ export async function handleDirectiveOnly(params: {
|
|||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (modelSelection) {
|
if (modelSelection) {
|
||||||
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
|
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||||
import { resolveProfileOverride } from "./directive-handling.auth.js";
|
import { resolveProfileOverride } from "./directive-handling.auth.js";
|
||||||
@ -184,7 +184,9 @@ export async function persistInlineDirectives(params: {
|
|||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (elevatedChanged) {
|
if (elevatedChanged) {
|
||||||
const nextElevated = (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel;
|
const nextElevated = (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel;
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import type { ClawdbotConfig } from "../../config/config.js";
|
|||||||
import {
|
import {
|
||||||
resolveSessionFilePath,
|
resolveSessionFilePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
import { clearCommandLane, getQueueSize } from "../../process/command-queue.js";
|
import { clearCommandLane, getQueueSize } from "../../process/command-queue.js";
|
||||||
@ -276,7 +276,9 @@ export async function runPreparedReply(
|
|||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
resolveThinkingDefault,
|
resolveThinkingDefault,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||||
import type { ThinkLevel } from "./directives.js";
|
import type { ThinkLevel } from "./directives.js";
|
||||||
|
|
||||||
export type ModelDirectiveSelection = {
|
export type ModelDirectiveSelection = {
|
||||||
@ -189,7 +189,9 @@ export async function createModelSelectionState(params: {
|
|||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
resetModelOverride = true;
|
resetModelOverride = true;
|
||||||
}
|
}
|
||||||
@ -218,7 +220,9 @@ export async function createModelSelectionState(params: {
|
|||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|||||||
|
|
||||||
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||||
import { buildChannelSummary } from "../../infra/channel-summary.js";
|
import { buildChannelSummary } from "../../infra/channel-summary.js";
|
||||||
import { drainSystemEventEntries } from "../../infra/system-events.js";
|
import { drainSystemEventEntries } from "../../infra/system-events.js";
|
||||||
|
|
||||||
@ -111,7 +111,9 @@ export async function ensureSkillSnapshot(params: {
|
|||||||
};
|
};
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...nextEntry };
|
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...nextEntry };
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = { ...store[sessionKey], ...nextEntry };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
systemSent = true;
|
systemSent = true;
|
||||||
}
|
}
|
||||||
@ -143,7 +145,9 @@ export async function ensureSkillSnapshot(params: {
|
|||||||
};
|
};
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...nextEntry };
|
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...nextEntry };
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = { ...store[sessionKey], ...nextEntry };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +172,13 @@ export async function incrementCompactionCount(params: {
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = {
|
||||||
|
...store[sessionKey],
|
||||||
|
compactionCount: nextCount,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return nextCount;
|
return nextCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import {
|
|||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
type SessionScope,
|
type SessionScope,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||||
@ -188,6 +188,11 @@ export async function initSessionState(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseEntry = !isNewSession && freshEntry ? entry : undefined;
|
const baseEntry = !isNewSession && freshEntry ? entry : undefined;
|
||||||
|
// Track the originating channel/to for announce routing (subagent announce-back).
|
||||||
|
const lastChannel =
|
||||||
|
(ctx.OriginatingChannel as string | undefined)?.trim() || baseEntry?.lastChannel;
|
||||||
|
const lastTo = ctx.OriginatingTo?.trim() || ctx.To?.trim() || baseEntry?.lastTo;
|
||||||
|
const lastAccountId = ctx.AccountId?.trim() || baseEntry?.lastAccountId;
|
||||||
sessionEntry = {
|
sessionEntry = {
|
||||||
...baseEntry,
|
...baseEntry,
|
||||||
sessionId,
|
sessionId,
|
||||||
@ -212,6 +217,10 @@ export async function initSessionState(params: {
|
|||||||
subject: baseEntry?.subject,
|
subject: baseEntry?.subject,
|
||||||
room: baseEntry?.room,
|
room: baseEntry?.room,
|
||||||
space: baseEntry?.space,
|
space: baseEntry?.space,
|
||||||
|
// Track originating channel for subagent announce routing.
|
||||||
|
lastChannel,
|
||||||
|
lastTo,
|
||||||
|
lastAccountId,
|
||||||
};
|
};
|
||||||
if (groupResolution?.channel) {
|
if (groupResolution?.channel) {
|
||||||
const channel = groupResolution.channel;
|
const channel = groupResolution.channel;
|
||||||
@ -270,7 +279,15 @@ export async function initSessionState(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
if (groupResolution?.legacyKey && groupResolution.legacyKey !== sessionKey) {
|
||||||
|
if (store[groupResolution.legacyKey] && !store[sessionKey]) {
|
||||||
|
store[sessionKey] = store[groupResolution.legacyKey];
|
||||||
|
}
|
||||||
|
delete store[groupResolution.legacyKey];
|
||||||
|
}
|
||||||
|
store[sessionKey] = { ...store[sessionKey], ...sessionEntry };
|
||||||
|
});
|
||||||
|
|
||||||
const sessionCtx: TemplateContext = {
|
const sessionCtx: TemplateContext = {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
|||||||
@ -36,7 +36,7 @@ import {
|
|||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
resolveSessionFilePath,
|
resolveSessionFilePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import {
|
import {
|
||||||
clearAgentRunContext,
|
clearAgentRunContext,
|
||||||
@ -173,7 +173,9 @@ export async function agentCommand(
|
|||||||
skillsSnapshot,
|
skillsSnapshot,
|
||||||
};
|
};
|
||||||
sessionStore[sessionKey] = next;
|
sessionStore[sessionKey] = next;
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = next;
|
||||||
|
});
|
||||||
sessionEntry = next;
|
sessionEntry = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +190,9 @@ export async function agentCommand(
|
|||||||
}
|
}
|
||||||
applyVerboseOverride(next, verboseOverride);
|
applyVerboseOverride(next, verboseOverride);
|
||||||
sessionStore[sessionKey] = next;
|
sessionStore[sessionKey] = next;
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentModelPrimary = resolveAgentModelPrimary(cfg, sessionAgentId);
|
const agentModelPrimary = resolveAgentModelPrimary(cfg, sessionAgentId);
|
||||||
@ -252,7 +256,9 @@ export async function agentCommand(
|
|||||||
delete sessionEntry.modelOverride;
|
delete sessionEntry.modelOverride;
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,7 +285,9 @@ export async function agentCommand(
|
|||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
if (sessionStore && sessionKey) {
|
if (sessionStore && sessionKey) {
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,7 +315,9 @@ export async function agentCommand(
|
|||||||
sessionEntry.thinkingLevel = "high";
|
sessionEntry.thinkingLevel = "high";
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, {
|
const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
|||||||
import { isCliProvider } from "../../agents/model-selection.js";
|
import { isCliProvider } from "../../agents/model-selection.js";
|
||||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
import { hasNonzeroUsage } from "../../agents/usage.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||||
|
|
||||||
type RunResult = Awaited<
|
type RunResult = Awaited<
|
||||||
ReturnType<(typeof import("../../agents/pi-embedded.js"))["runEmbeddedPiAgent"]>
|
ReturnType<(typeof import("../../agents/pi-embedded.js"))["runEmbeddedPiAgent"]>
|
||||||
@ -68,5 +68,7 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
|||||||
next.totalTokens = promptTokens > 0 ? promptTokens : (usage.total ?? input);
|
next.totalTokens = promptTokens > 0 ? promptTokens : (usage.total ?? input);
|
||||||
}
|
}
|
||||||
sessionStore[sessionKey] = next;
|
sessionStore[sessionKey] = next;
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[sessionKey] = next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
resolveSessionTranscriptPath,
|
resolveSessionTranscriptPath,
|
||||||
resolveSessionTranscriptsDir,
|
resolveSessionTranscriptsDir,
|
||||||
updateLastRoute,
|
updateLastRoute,
|
||||||
|
updateSessionStore,
|
||||||
updateSessionStoreEntry,
|
updateSessionStoreEntry,
|
||||||
} from "./sessions.js";
|
} from "./sessions.js";
|
||||||
|
|
||||||
@ -137,6 +138,56 @@ describe("sessions", () => {
|
|||||||
expect(store[mainSessionKey]?.compactionCount).toBe(2);
|
expect(store[mainSessionKey]?.compactionCount).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updateSessionStore preserves concurrent additions", async () => {
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
||||||
|
const storePath = path.join(dir, "sessions.json");
|
||||||
|
await fs.writeFile(storePath, "{}", "utf-8");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
updateSessionStore(storePath, (store) => {
|
||||||
|
store["agent:main:one"] = { sessionId: "sess-1", updatedAt: 1 };
|
||||||
|
}),
|
||||||
|
updateSessionStore(storePath, (store) => {
|
||||||
|
store["agent:main:two"] = { sessionId: "sess-2", updatedAt: 2 };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const store = loadSessionStore(storePath);
|
||||||
|
expect(store["agent:main:one"]?.sessionId).toBe("sess-1");
|
||||||
|
expect(store["agent:main:two"]?.sessionId).toBe("sess-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updateSessionStore keeps deletions when concurrent writes happen", async () => {
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
||||||
|
const storePath = path.join(dir, "sessions.json");
|
||||||
|
await fs.writeFile(
|
||||||
|
storePath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
"agent:main:old": { sessionId: "sess-old", updatedAt: 1 },
|
||||||
|
"agent:main:keep": { sessionId: "sess-keep", updatedAt: 2 },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
updateSessionStore(storePath, (store) => {
|
||||||
|
delete store["agent:main:old"];
|
||||||
|
}),
|
||||||
|
updateSessionStore(storePath, (store) => {
|
||||||
|
store["agent:main:new"] = { sessionId: "sess-new", updatedAt: 3 };
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const store = loadSessionStore(storePath);
|
||||||
|
expect(store["agent:main:old"]).toBeUndefined();
|
||||||
|
expect(store["agent:main:keep"]?.sessionId).toBe("sess-keep");
|
||||||
|
expect(store["agent:main:new"]?.sessionId).toBe("sess-new");
|
||||||
|
});
|
||||||
|
|
||||||
it("loadSessionStore auto-migrates legacy provider keys to channel keys", async () => {
|
it("loadSessionStore auto-migrates legacy provider keys to channel keys", async () => {
|
||||||
const mainSessionKey = "agent:main:main";
|
const mainSessionKey = "agent:main:main";
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
||||||
|
|||||||
@ -45,9 +45,16 @@ export function clearSessionStoreCacheForTest(): void {
|
|||||||
SESSION_STORE_CACHE.clear();
|
SESSION_STORE_CACHE.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadSessionStore(storePath: string): Record<string, SessionEntry> {
|
type LoadSessionStoreOptions = {
|
||||||
|
skipCache?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function loadSessionStore(
|
||||||
|
storePath: string,
|
||||||
|
opts: LoadSessionStoreOptions = {},
|
||||||
|
): Record<string, SessionEntry> {
|
||||||
// Check cache first if enabled
|
// Check cache first if enabled
|
||||||
if (isSessionStoreCacheEnabled()) {
|
if (!opts.skipCache && isSessionStoreCacheEnabled()) {
|
||||||
const cached = SESSION_STORE_CACHE.get(storePath);
|
const cached = SESSION_STORE_CACHE.get(storePath);
|
||||||
if (cached && isSessionStoreCacheValid(cached)) {
|
if (cached && isSessionStoreCacheValid(cached)) {
|
||||||
const currentMtimeMs = getFileMtimeMs(storePath);
|
const currentMtimeMs = getFileMtimeMs(storePath);
|
||||||
@ -88,7 +95,7 @@ export function loadSessionStore(storePath: string): Record<string, SessionEntry
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache the result if caching is enabled
|
// Cache the result if caching is enabled
|
||||||
if (isSessionStoreCacheEnabled()) {
|
if (!opts.skipCache && isSessionStoreCacheEnabled()) {
|
||||||
SESSION_STORE_CACHE.set(storePath, {
|
SESSION_STORE_CACHE.set(storePath, {
|
||||||
store: structuredClone(store), // Store a copy to prevent external mutations
|
store: structuredClone(store), // Store a copy to prevent external mutations
|
||||||
loadedAt: Date.now(),
|
loadedAt: Date.now(),
|
||||||
@ -168,6 +175,19 @@ export async function saveSessionStore(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateSessionStore<T>(
|
||||||
|
storePath: string,
|
||||||
|
mutator: (store: Record<string, SessionEntry>) => Promise<T> | T,
|
||||||
|
): Promise<T> {
|
||||||
|
return await withSessionStoreLock(storePath, async () => {
|
||||||
|
// Always re-read inside the lock to avoid clobbering concurrent writers.
|
||||||
|
const store = loadSessionStore(storePath, { skipCache: true });
|
||||||
|
const result = await mutator(store);
|
||||||
|
await saveSessionStoreUnlocked(storePath, store);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type SessionStoreLockOptions = {
|
type SessionStoreLockOptions = {
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
pollIntervalMs?: number;
|
pollIntervalMs?: number;
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {
|
|||||||
} from "../../auto-reply/thinking.js";
|
} from "../../auto-reply/thinking.js";
|
||||||
import type { CliDeps } from "../../cli/deps.js";
|
import type { CliDeps } from "../../cli/deps.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { resolveSessionTranscriptPath, saveSessionStore } from "../../config/sessions.js";
|
import { resolveSessionTranscriptPath, updateSessionStore } from "../../config/sessions.js";
|
||||||
import type { AgentDefaultsConfig } from "../../config/types.js";
|
import type { AgentDefaultsConfig } from "../../config/types.js";
|
||||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||||
import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js";
|
import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js";
|
||||||
@ -217,13 +217,17 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
skillsSnapshot,
|
skillsSnapshot,
|
||||||
};
|
};
|
||||||
cronSession.store[agentSessionKey] = cronSession.sessionEntry;
|
cronSession.store[agentSessionKey] = cronSession.sessionEntry;
|
||||||
await saveSessionStore(cronSession.storePath, cronSession.store);
|
await updateSessionStore(cronSession.storePath, (store) => {
|
||||||
|
store[agentSessionKey] = cronSession.sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist systemSent before the run, mirroring the inbound auto-reply behavior.
|
// Persist systemSent before the run, mirroring the inbound auto-reply behavior.
|
||||||
cronSession.sessionEntry.systemSent = true;
|
cronSession.sessionEntry.systemSent = true;
|
||||||
cronSession.store[agentSessionKey] = cronSession.sessionEntry;
|
cronSession.store[agentSessionKey] = cronSession.sessionEntry;
|
||||||
await saveSessionStore(cronSession.storePath, cronSession.store);
|
await updateSessionStore(cronSession.storePath, (store) => {
|
||||||
|
store[agentSessionKey] = cronSession.sessionEntry;
|
||||||
|
});
|
||||||
|
|
||||||
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
|
||||||
let fallbackProvider = provider;
|
let fallbackProvider = provider;
|
||||||
@ -316,7 +320,9 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
promptTokens > 0 ? promptTokens : (usage.total ?? input);
|
promptTokens > 0 ? promptTokens : (usage.total ?? input);
|
||||||
}
|
}
|
||||||
cronSession.store[agentSessionKey] = cronSession.sessionEntry;
|
cronSession.store[agentSessionKey] = cronSession.sessionEntry;
|
||||||
await saveSessionStore(cronSession.storePath, cronSession.store);
|
await updateSessionStore(cronSession.storePath, (store) => {
|
||||||
|
store[agentSessionKey] = cronSession.sessionEntry;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const firstText = payloads[0]?.text ?? "";
|
const firstText = payloads[0]?.text ?? "";
|
||||||
const summary = pickSummaryFromPayloads(payloads) ?? pickSummaryFromOutput(firstText);
|
const summary = pickSummaryFromPayloads(payloads) ?? pickSummaryFromOutput(firstText);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { normalizeChannelId } from "../channels/plugins/index.js";
|
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import { agentCommand } from "../commands/agent.js";
|
import { agentCommand } from "../commands/agent.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { saveSessionStore } from "../config/sessions.js";
|
import { updateSessionStore } from "../config/sessions.js";
|
||||||
import { normalizeMainKey } from "../routing/session-key.js";
|
import { normalizeMainKey } from "../routing/session-key.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import type { BridgeEvent, BridgeHandlersContext } from "./server-bridge-types.js";
|
import type { BridgeEvent, BridgeHandlersContext } from "./server-bridge-types.js";
|
||||||
@ -32,22 +32,23 @@ export const handleBridgeEvent = async (
|
|||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const rawMainKey = normalizeMainKey(cfg.session?.mainKey);
|
const rawMainKey = normalizeMainKey(cfg.session?.mainKey);
|
||||||
const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : rawMainKey;
|
const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : rawMainKey;
|
||||||
const { storePath, store, entry, canonicalKey } = loadSessionEntry(sessionKey);
|
const { storePath, entry, canonicalKey } = loadSessionEntry(sessionKey);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const sessionId = entry?.sessionId ?? randomUUID();
|
const sessionId = entry?.sessionId ?? randomUUID();
|
||||||
store[canonicalKey] = {
|
|
||||||
sessionId,
|
|
||||||
updatedAt: now,
|
|
||||||
thinkingLevel: entry?.thinkingLevel,
|
|
||||||
verboseLevel: entry?.verboseLevel,
|
|
||||||
reasoningLevel: entry?.reasoningLevel,
|
|
||||||
systemSent: entry?.systemSent,
|
|
||||||
sendPolicy: entry?.sendPolicy,
|
|
||||||
lastChannel: entry?.lastChannel,
|
|
||||||
lastTo: entry?.lastTo,
|
|
||||||
};
|
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, store);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[canonicalKey] = {
|
||||||
|
sessionId,
|
||||||
|
updatedAt: now,
|
||||||
|
thinkingLevel: entry?.thinkingLevel,
|
||||||
|
verboseLevel: entry?.verboseLevel,
|
||||||
|
reasoningLevel: entry?.reasoningLevel,
|
||||||
|
systemSent: entry?.systemSent,
|
||||||
|
sendPolicy: entry?.sendPolicy,
|
||||||
|
lastChannel: entry?.lastChannel,
|
||||||
|
lastTo: entry?.lastTo,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure chat UI clients refresh when this run completes (even though it wasn't started via chat.send).
|
// Ensure chat UI clients refresh when this run completes (even though it wasn't started via chat.send).
|
||||||
@ -102,22 +103,23 @@ export const handleBridgeEvent = async (
|
|||||||
|
|
||||||
const sessionKeyRaw = (link?.sessionKey ?? "").trim();
|
const sessionKeyRaw = (link?.sessionKey ?? "").trim();
|
||||||
const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : `node-${nodeId}`;
|
const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : `node-${nodeId}`;
|
||||||
const { storePath, store, entry, canonicalKey } = loadSessionEntry(sessionKey);
|
const { storePath, entry, canonicalKey } = loadSessionEntry(sessionKey);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const sessionId = entry?.sessionId ?? randomUUID();
|
const sessionId = entry?.sessionId ?? randomUUID();
|
||||||
store[canonicalKey] = {
|
|
||||||
sessionId,
|
|
||||||
updatedAt: now,
|
|
||||||
thinkingLevel: entry?.thinkingLevel,
|
|
||||||
verboseLevel: entry?.verboseLevel,
|
|
||||||
reasoningLevel: entry?.reasoningLevel,
|
|
||||||
systemSent: entry?.systemSent,
|
|
||||||
sendPolicy: entry?.sendPolicy,
|
|
||||||
lastChannel: entry?.lastChannel,
|
|
||||||
lastTo: entry?.lastTo,
|
|
||||||
};
|
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, store);
|
await updateSessionStore(storePath, (store) => {
|
||||||
|
store[canonicalKey] = {
|
||||||
|
sessionId,
|
||||||
|
updatedAt: now,
|
||||||
|
thinkingLevel: entry?.thinkingLevel,
|
||||||
|
verboseLevel: entry?.verboseLevel,
|
||||||
|
reasoningLevel: entry?.reasoningLevel,
|
||||||
|
systemSent: entry?.systemSent,
|
||||||
|
sendPolicy: entry?.sendPolicy,
|
||||||
|
lastChannel: entry?.lastChannel,
|
||||||
|
lastTo: entry?.lastTo,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void agentCommand(
|
void agentCommand(
|
||||||
|
|||||||
@ -8,10 +8,9 @@ import {
|
|||||||
} from "../agents/pi-embedded.js";
|
} from "../agents/pi-embedded.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
loadSessionStore,
|
|
||||||
resolveMainSessionKeyFromConfig,
|
resolveMainSessionKeyFromConfig,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { clearCommandLane } from "../process/command-queue.js";
|
import { clearCommandLane } from "../process/command-queue.js";
|
||||||
import {
|
import {
|
||||||
@ -126,19 +125,20 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async (
|
|||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||||
const storePath = target.storePath;
|
const storePath = target.storePath;
|
||||||
const store = loadSessionStore(storePath);
|
const applied = await updateSessionStore(storePath, async (store) => {
|
||||||
const primaryKey = target.storeKeys[0] ?? key;
|
const primaryKey = target.storeKeys[0] ?? key;
|
||||||
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
||||||
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
||||||
store[primaryKey] = store[existingKey];
|
store[primaryKey] = store[existingKey];
|
||||||
delete store[existingKey];
|
delete store[existingKey];
|
||||||
}
|
}
|
||||||
const applied = await applySessionsPatchToStore({
|
return await applySessionsPatchToStore({
|
||||||
cfg,
|
cfg,
|
||||||
store,
|
store,
|
||||||
storeKey: primaryKey,
|
storeKey: primaryKey,
|
||||||
patch: p,
|
patch: p,
|
||||||
loadGatewayModelCatalog: ctx.loadGatewayModelCatalog,
|
loadGatewayModelCatalog: ctx.loadGatewayModelCatalog,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (!applied.ok) {
|
if (!applied.ok) {
|
||||||
return {
|
return {
|
||||||
@ -150,7 +150,6 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await saveSessionStore(storePath, store);
|
|
||||||
const payload: SessionsPatchResult = {
|
const payload: SessionsPatchResult = {
|
||||||
ok: true,
|
ok: true,
|
||||||
path: storePath,
|
path: storePath,
|
||||||
@ -182,32 +181,43 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { storePath, store, entry } = loadSessionEntry(key);
|
const cfg = loadConfig();
|
||||||
const now = Date.now();
|
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||||
const next: SessionEntry = {
|
const storePath = target.storePath;
|
||||||
sessionId: randomUUID(),
|
const next = await updateSessionStore(storePath, (store) => {
|
||||||
updatedAt: now,
|
const primaryKey = target.storeKeys[0] ?? key;
|
||||||
systemSent: false,
|
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
||||||
abortedLastRun: false,
|
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
||||||
thinkingLevel: entry?.thinkingLevel,
|
store[primaryKey] = store[existingKey];
|
||||||
verboseLevel: entry?.verboseLevel,
|
delete store[existingKey];
|
||||||
reasoningLevel: entry?.reasoningLevel,
|
}
|
||||||
model: entry?.model,
|
const entry = store[primaryKey];
|
||||||
contextTokens: entry?.contextTokens,
|
const now = Date.now();
|
||||||
sendPolicy: entry?.sendPolicy,
|
const nextEntry: SessionEntry = {
|
||||||
label: entry?.label,
|
sessionId: randomUUID(),
|
||||||
displayName: entry?.displayName,
|
updatedAt: now,
|
||||||
chatType: entry?.chatType,
|
systemSent: false,
|
||||||
channel: entry?.channel,
|
abortedLastRun: false,
|
||||||
subject: entry?.subject,
|
thinkingLevel: entry?.thinkingLevel,
|
||||||
room: entry?.room,
|
verboseLevel: entry?.verboseLevel,
|
||||||
space: entry?.space,
|
reasoningLevel: entry?.reasoningLevel,
|
||||||
lastChannel: entry?.lastChannel,
|
model: entry?.model,
|
||||||
lastTo: entry?.lastTo,
|
contextTokens: entry?.contextTokens,
|
||||||
skillsSnapshot: entry?.skillsSnapshot,
|
sendPolicy: entry?.sendPolicy,
|
||||||
};
|
label: entry?.label,
|
||||||
store[key] = next;
|
displayName: entry?.displayName,
|
||||||
await saveSessionStore(storePath, store);
|
chatType: entry?.chatType,
|
||||||
|
channel: entry?.channel,
|
||||||
|
subject: entry?.subject,
|
||||||
|
room: entry?.room,
|
||||||
|
space: entry?.space,
|
||||||
|
lastChannel: entry?.lastChannel,
|
||||||
|
lastTo: entry?.lastTo,
|
||||||
|
skillsSnapshot: entry?.skillsSnapshot,
|
||||||
|
};
|
||||||
|
store[primaryKey] = nextEntry;
|
||||||
|
return nextEntry;
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
payloadJSON: JSON.stringify({ ok: true, key, entry: next }),
|
payloadJSON: JSON.stringify({ ok: true, key, entry: next }),
|
||||||
@ -249,9 +259,11 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async (
|
|||||||
|
|
||||||
const deleteTranscript = typeof p.deleteTranscript === "boolean" ? p.deleteTranscript : true;
|
const deleteTranscript = typeof p.deleteTranscript === "boolean" ? p.deleteTranscript : true;
|
||||||
|
|
||||||
const { storePath, store, entry } = loadSessionEntry(key);
|
const cfg = loadConfig();
|
||||||
|
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||||
|
const storePath = target.storePath;
|
||||||
|
const { entry } = loadSessionEntry(key);
|
||||||
const sessionId = entry?.sessionId;
|
const sessionId = entry?.sessionId;
|
||||||
const existed = Boolean(store[key]);
|
|
||||||
clearCommandLane(resolveEmbeddedSessionLane(key));
|
clearCommandLane(resolveEmbeddedSessionLane(key));
|
||||||
if (sessionId && isEmbeddedPiRunActive(sessionId)) {
|
if (sessionId && isEmbeddedPiRunActive(sessionId)) {
|
||||||
abortEmbeddedPiRun(sessionId);
|
abortEmbeddedPiRun(sessionId);
|
||||||
@ -266,8 +278,19 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (existed) delete store[key];
|
const deletion = await updateSessionStore(storePath, (store) => {
|
||||||
await saveSessionStore(storePath, store);
|
const primaryKey = target.storeKeys[0] ?? key;
|
||||||
|
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
||||||
|
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
||||||
|
store[primaryKey] = store[existingKey];
|
||||||
|
delete store[existingKey];
|
||||||
|
}
|
||||||
|
const entryToDelete = store[primaryKey];
|
||||||
|
const existed = Boolean(entryToDelete);
|
||||||
|
if (existed) delete store[primaryKey];
|
||||||
|
return { existed, entry: entryToDelete };
|
||||||
|
});
|
||||||
|
const existed = deletion.existed;
|
||||||
|
|
||||||
const archived: string[] = [];
|
const archived: string[] = [];
|
||||||
if (deleteTranscript && sessionId) {
|
if (deleteTranscript && sessionId) {
|
||||||
@ -323,7 +346,20 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async (
|
|||||||
? Math.max(1, Math.floor(p.maxLines))
|
? Math.max(1, Math.floor(p.maxLines))
|
||||||
: 400;
|
: 400;
|
||||||
|
|
||||||
const { storePath, store, entry } = loadSessionEntry(key);
|
const cfg = loadConfig();
|
||||||
|
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||||
|
const storePath = target.storePath;
|
||||||
|
// Resolve entry inside the lock, but compact outside to avoid holding it.
|
||||||
|
const compactTarget = await updateSessionStore(storePath, (store) => {
|
||||||
|
const primaryKey = target.storeKeys[0] ?? key;
|
||||||
|
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
||||||
|
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
||||||
|
store[primaryKey] = store[existingKey];
|
||||||
|
delete store[existingKey];
|
||||||
|
}
|
||||||
|
return { entry: store[primaryKey], primaryKey };
|
||||||
|
});
|
||||||
|
const entry = compactTarget.entry;
|
||||||
const sessionId = entry?.sessionId;
|
const sessionId = entry?.sessionId;
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return {
|
return {
|
||||||
@ -373,13 +409,14 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async (
|
|||||||
fs.writeFileSync(filePath, `${keptLines.join("\n")}\n`, "utf-8");
|
fs.writeFileSync(filePath, `${keptLines.join("\n")}\n`, "utf-8");
|
||||||
|
|
||||||
// Token counts no longer match; clear so status + UI reflect reality after the next turn.
|
// Token counts no longer match; clear so status + UI reflect reality after the next turn.
|
||||||
if (store[key]) {
|
await updateSessionStore(storePath, (store) => {
|
||||||
delete store[key].inputTokens;
|
const entryToUpdate = store[compactTarget.primaryKey];
|
||||||
delete store[key].outputTokens;
|
if (!entryToUpdate) return;
|
||||||
delete store[key].totalTokens;
|
delete entryToUpdate.inputTokens;
|
||||||
store[key].updatedAt = Date.now();
|
delete entryToUpdate.outputTokens;
|
||||||
await saveSessionStore(storePath, store);
|
delete entryToUpdate.totalTokens;
|
||||||
}
|
entryToUpdate.updatedAt = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
resolveAgentMainSessionKey,
|
resolveAgentMainSessionKey,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||||
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
||||||
@ -136,7 +136,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
let cfgForAgent: ReturnType<typeof loadConfig> | undefined;
|
let cfgForAgent: ReturnType<typeof loadConfig> | undefined;
|
||||||
|
|
||||||
if (requestedSessionKey) {
|
if (requestedSessionKey) {
|
||||||
const { cfg, storePath, store, entry, canonicalKey } = loadSessionEntry(requestedSessionKey);
|
const { cfg, storePath, entry, canonicalKey } = loadSessionEntry(requestedSessionKey);
|
||||||
cfgForAgent = cfg;
|
cfgForAgent = cfg;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const sessionId = entry?.sessionId ?? randomUUID();
|
const sessionId = entry?.sessionId ?? randomUUID();
|
||||||
@ -178,11 +178,10 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
const canonicalSessionKey = canonicalKey;
|
const canonicalSessionKey = canonicalKey;
|
||||||
const agentId = resolveAgentIdFromSessionKey(canonicalSessionKey);
|
const agentId = resolveAgentIdFromSessionKey(canonicalSessionKey);
|
||||||
const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId });
|
const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId });
|
||||||
if (store) {
|
if (storePath) {
|
||||||
store[canonicalSessionKey] = nextEntry;
|
await updateSessionStore(storePath, (store) => {
|
||||||
if (storePath) {
|
store[canonicalSessionKey] = nextEntry;
|
||||||
await saveSessionStore(storePath, store);
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (canonicalSessionKey === mainSessionKey || canonicalSessionKey === "global") {
|
if (canonicalSessionKey === mainSessionKey || canonicalSessionKey === "global") {
|
||||||
context.addChatRun(idem, {
|
context.addChatRun(idem, {
|
||||||
|
|||||||
@ -9,10 +9,9 @@ import {
|
|||||||
} from "../../agents/pi-embedded.js";
|
} from "../../agents/pi-embedded.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
loadSessionStore,
|
|
||||||
resolveMainSessionKey,
|
resolveMainSessionKey,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { clearCommandLane } from "../../process/command-queue.js";
|
import { clearCommandLane } from "../../process/command-queue.js";
|
||||||
import {
|
import {
|
||||||
@ -30,6 +29,7 @@ import {
|
|||||||
archiveFileOnDisk,
|
archiveFileOnDisk,
|
||||||
listSessionsFromStore,
|
listSessionsFromStore,
|
||||||
loadCombinedSessionStoreForGateway,
|
loadCombinedSessionStoreForGateway,
|
||||||
|
loadSessionEntry,
|
||||||
resolveGatewaySessionStoreTarget,
|
resolveGatewaySessionStoreTarget,
|
||||||
resolveSessionTranscriptCandidates,
|
resolveSessionTranscriptCandidates,
|
||||||
type SessionsPatchResult,
|
type SessionsPatchResult,
|
||||||
@ -106,26 +106,25 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||||
const storePath = target.storePath;
|
const storePath = target.storePath;
|
||||||
const store = loadSessionStore(storePath);
|
const applied = await updateSessionStore(storePath, async (store) => {
|
||||||
|
const primaryKey = target.storeKeys[0] ?? key;
|
||||||
const primaryKey = target.storeKeys[0] ?? key;
|
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
||||||
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
||||||
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
store[primaryKey] = store[existingKey];
|
||||||
store[primaryKey] = store[existingKey];
|
delete store[existingKey];
|
||||||
delete store[existingKey];
|
}
|
||||||
}
|
return await applySessionsPatchToStore({
|
||||||
const applied = await applySessionsPatchToStore({
|
cfg,
|
||||||
cfg,
|
store,
|
||||||
store,
|
storeKey: primaryKey,
|
||||||
storeKey: primaryKey,
|
patch: p,
|
||||||
patch: p,
|
loadGatewayModelCatalog: context.loadGatewayModelCatalog,
|
||||||
loadGatewayModelCatalog: context.loadGatewayModelCatalog,
|
});
|
||||||
});
|
});
|
||||||
if (!applied.ok) {
|
if (!applied.ok) {
|
||||||
respond(false, undefined, applied.error);
|
respond(false, undefined, applied.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await saveSessionStore(storePath, store);
|
|
||||||
const result: SessionsPatchResult = {
|
const result: SessionsPatchResult = {
|
||||||
ok: true,
|
ok: true,
|
||||||
path: storePath,
|
path: storePath,
|
||||||
@ -156,34 +155,35 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||||
const storePath = target.storePath;
|
const storePath = target.storePath;
|
||||||
const store = loadSessionStore(storePath);
|
const next = await updateSessionStore(storePath, (store) => {
|
||||||
const primaryKey = target.storeKeys[0] ?? key;
|
const primaryKey = target.storeKeys[0] ?? key;
|
||||||
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
||||||
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
||||||
store[primaryKey] = store[existingKey];
|
store[primaryKey] = store[existingKey];
|
||||||
delete store[existingKey];
|
delete store[existingKey];
|
||||||
}
|
}
|
||||||
const entry = store[primaryKey];
|
const entry = store[primaryKey];
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const next: SessionEntry = {
|
const nextEntry: SessionEntry = {
|
||||||
sessionId: randomUUID(),
|
sessionId: randomUUID(),
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
systemSent: false,
|
systemSent: false,
|
||||||
abortedLastRun: false,
|
abortedLastRun: false,
|
||||||
thinkingLevel: entry?.thinkingLevel,
|
thinkingLevel: entry?.thinkingLevel,
|
||||||
verboseLevel: entry?.verboseLevel,
|
verboseLevel: entry?.verboseLevel,
|
||||||
reasoningLevel: entry?.reasoningLevel,
|
reasoningLevel: entry?.reasoningLevel,
|
||||||
responseUsage: entry?.responseUsage,
|
responseUsage: entry?.responseUsage,
|
||||||
model: entry?.model,
|
model: entry?.model,
|
||||||
contextTokens: entry?.contextTokens,
|
contextTokens: entry?.contextTokens,
|
||||||
sendPolicy: entry?.sendPolicy,
|
sendPolicy: entry?.sendPolicy,
|
||||||
label: entry?.label,
|
label: entry?.label,
|
||||||
lastChannel: entry?.lastChannel,
|
lastChannel: entry?.lastChannel,
|
||||||
lastTo: entry?.lastTo,
|
lastTo: entry?.lastTo,
|
||||||
skillsSnapshot: entry?.skillsSnapshot,
|
skillsSnapshot: entry?.skillsSnapshot,
|
||||||
};
|
};
|
||||||
store[primaryKey] = next;
|
store[primaryKey] = nextEntry;
|
||||||
await saveSessionStore(storePath, store);
|
return nextEntry;
|
||||||
|
});
|
||||||
respond(true, { ok: true, key: target.canonicalKey, entry: next }, undefined);
|
respond(true, { ok: true, key: target.canonicalKey, entry: next }, undefined);
|
||||||
},
|
},
|
||||||
"sessions.delete": async ({ params, respond }) => {
|
"sessions.delete": async ({ params, respond }) => {
|
||||||
@ -220,14 +220,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
const deleteTranscript = typeof p.deleteTranscript === "boolean" ? p.deleteTranscript : true;
|
const deleteTranscript = typeof p.deleteTranscript === "boolean" ? p.deleteTranscript : true;
|
||||||
|
|
||||||
const storePath = target.storePath;
|
const storePath = target.storePath;
|
||||||
const store = loadSessionStore(storePath);
|
const { entry } = loadSessionEntry(key);
|
||||||
const primaryKey = target.storeKeys[0] ?? key;
|
|
||||||
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
|
||||||
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
|
||||||
store[primaryKey] = store[existingKey];
|
|
||||||
delete store[existingKey];
|
|
||||||
}
|
|
||||||
const entry = store[primaryKey];
|
|
||||||
const sessionId = entry?.sessionId;
|
const sessionId = entry?.sessionId;
|
||||||
const existed = Boolean(entry);
|
const existed = Boolean(entry);
|
||||||
clearCommandLane(resolveEmbeddedSessionLane(target.canonicalKey));
|
clearCommandLane(resolveEmbeddedSessionLane(target.canonicalKey));
|
||||||
@ -246,8 +239,15 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (existed) delete store[primaryKey];
|
await updateSessionStore(storePath, (store) => {
|
||||||
await saveSessionStore(storePath, store);
|
const primaryKey = target.storeKeys[0] ?? key;
|
||||||
|
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
||||||
|
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
||||||
|
store[primaryKey] = store[existingKey];
|
||||||
|
delete store[existingKey];
|
||||||
|
}
|
||||||
|
if (store[primaryKey]) delete store[primaryKey];
|
||||||
|
});
|
||||||
|
|
||||||
const archived: string[] = [];
|
const archived: string[] = [];
|
||||||
if (deleteTranscript && sessionId) {
|
if (deleteTranscript && sessionId) {
|
||||||
@ -295,14 +295,17 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
const target = resolveGatewaySessionStoreTarget({ cfg, key });
|
||||||
const storePath = target.storePath;
|
const storePath = target.storePath;
|
||||||
const store = loadSessionStore(storePath);
|
// Lock + read in a short critical section; transcript work happens outside.
|
||||||
const primaryKey = target.storeKeys[0] ?? key;
|
const compactTarget = await updateSessionStore(storePath, (store) => {
|
||||||
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
const primaryKey = target.storeKeys[0] ?? key;
|
||||||
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
const existingKey = target.storeKeys.find((candidate) => store[candidate]);
|
||||||
store[primaryKey] = store[existingKey];
|
if (existingKey && existingKey !== primaryKey && !store[primaryKey]) {
|
||||||
delete store[existingKey];
|
store[primaryKey] = store[existingKey];
|
||||||
}
|
delete store[existingKey];
|
||||||
const entry = store[primaryKey];
|
}
|
||||||
|
return { entry: store[primaryKey], primaryKey };
|
||||||
|
});
|
||||||
|
const entry = compactTarget.entry;
|
||||||
const sessionId = entry?.sessionId;
|
const sessionId = entry?.sessionId;
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
respond(
|
respond(
|
||||||
@ -358,13 +361,15 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
const keptLines = lines.slice(-maxLines);
|
const keptLines = lines.slice(-maxLines);
|
||||||
fs.writeFileSync(filePath, `${keptLines.join("\n")}\n`, "utf-8");
|
fs.writeFileSync(filePath, `${keptLines.join("\n")}\n`, "utf-8");
|
||||||
|
|
||||||
if (store[primaryKey]) {
|
await updateSessionStore(storePath, (store) => {
|
||||||
delete store[primaryKey].inputTokens;
|
const entryKey = compactTarget.primaryKey;
|
||||||
delete store[primaryKey].outputTokens;
|
const entryToUpdate = store[entryKey];
|
||||||
delete store[primaryKey].totalTokens;
|
if (!entryToUpdate) return;
|
||||||
store[primaryKey].updatedAt = Date.now();
|
delete entryToUpdate.inputTokens;
|
||||||
await saveSessionStore(storePath, store);
|
delete entryToUpdate.outputTokens;
|
||||||
}
|
delete entryToUpdate.totalTokens;
|
||||||
|
entryToUpdate.updatedAt = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
respond(
|
respond(
|
||||||
true,
|
true,
|
||||||
|
|||||||
@ -285,9 +285,12 @@ export function loadCombinedSessionStoreForGateway(cfg: ClawdbotConfig): {
|
|||||||
const store = loadSessionStore(storePath);
|
const store = loadSessionStore(storePath);
|
||||||
for (const [key, entry] of Object.entries(store)) {
|
for (const [key, entry] of Object.entries(store)) {
|
||||||
const canonicalKey = canonicalizeSessionKeyForAgent(agentId, key);
|
const canonicalKey = canonicalizeSessionKeyForAgent(agentId, key);
|
||||||
|
// Merge with existing entry if present (avoid overwriting with less complete data)
|
||||||
|
const existing = combined[canonicalKey];
|
||||||
combined[canonicalKey] = {
|
combined[canonicalKey] = {
|
||||||
|
...existing,
|
||||||
...entry,
|
...entry,
|
||||||
spawnedBy: canonicalizeSpawnedByForAgent(agentId, entry.spawnedBy),
|
spawnedBy: canonicalizeSpawnedByForAgent(agentId, entry.spawnedBy ?? existing?.spawnedBy),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
resolveMainSessionKey,
|
resolveMainSessionKey,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { formatErrorMessage } from "../infra/errors.js";
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
import { createSubsystemLogger } from "../logging.js";
|
import { createSubsystemLogger } from "../logging.js";
|
||||||
@ -150,8 +150,13 @@ async function restoreHeartbeatUpdatedAt(params: {
|
|||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
const nextUpdatedAt = Math.max(entry.updatedAt ?? 0, updatedAt);
|
const nextUpdatedAt = Math.max(entry.updatedAt ?? 0, updatedAt);
|
||||||
if (entry.updatedAt === nextUpdatedAt) return;
|
if (entry.updatedAt === nextUpdatedAt) return;
|
||||||
store[sessionKey] = { ...entry, updatedAt: nextUpdatedAt };
|
await updateSessionStore(storePath, (nextStore) => {
|
||||||
await saveSessionStore(storePath, store);
|
const nextEntry = nextStore[sessionKey] ?? entry;
|
||||||
|
if (!nextEntry) return;
|
||||||
|
const resolvedUpdatedAt = Math.max(nextEntry.updatedAt ?? 0, updatedAt);
|
||||||
|
if (nextEntry.updatedAt === resolvedUpdatedAt) return;
|
||||||
|
nextStore[sessionKey] = { ...nextEntry, updatedAt: resolvedUpdatedAt };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeHeartbeatReply(
|
function normalizeHeartbeatReply(
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
resolveSessionKey,
|
resolveSessionKey,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
saveSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { emitHeartbeatEvent } from "../../infra/heartbeat-events.js";
|
import { emitHeartbeatEvent } from "../../infra/heartbeat-events.js";
|
||||||
import { getChildLogger } from "../../logging.js";
|
import { getChildLogger } from "../../logging.js";
|
||||||
@ -72,7 +72,14 @@ export async function runWebHeartbeatOnce(opts: {
|
|||||||
sessionId,
|
sessionId,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
};
|
};
|
||||||
await saveSessionStore(storePath, store);
|
await updateSessionStore(storePath, (nextStore) => {
|
||||||
|
const nextCurrent = nextStore[sessionKey] ?? current;
|
||||||
|
nextStore[sessionKey] = {
|
||||||
|
...nextCurrent,
|
||||||
|
sessionId,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const sessionSnapshot = getSessionSnapshot(cfg, to, true);
|
const sessionSnapshot = getSessionSnapshot(cfg, to, true);
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
@ -163,7 +170,14 @@ export async function runWebHeartbeatOnce(opts: {
|
|||||||
const store = loadSessionStore(storePath);
|
const store = loadSessionStore(storePath);
|
||||||
if (sessionSnapshot.entry && store[sessionSnapshot.key]) {
|
if (sessionSnapshot.entry && store[sessionSnapshot.key]) {
|
||||||
store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
|
store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
|
||||||
await saveSessionStore(storePath, store);
|
await updateSessionStore(storePath, (nextStore) => {
|
||||||
|
const nextEntry = nextStore[sessionSnapshot.key];
|
||||||
|
if (!nextEntry) return;
|
||||||
|
nextStore[sessionSnapshot.key] = {
|
||||||
|
...nextEntry,
|
||||||
|
updatedAt: sessionSnapshot.entry.updatedAt,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
heartbeatLogger.info(
|
heartbeatLogger.info(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user