惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
The GitHub Blog
The GitHub Blog
M
MIT News - Artificial intelligence
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏
Last Week in AI
Last Week in AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
罗磊的独立博客
H
Help Net Security
Engineering at Meta
Engineering at Meta
Microsoft Security Blog
Microsoft Security Blog
阮一峰的网络日志
阮一峰的网络日志
J
Java Code Geeks
T
The Blog of Author Tim Ferriss
Hugging Face - Blog
Hugging Face - Blog
C
Check Point Blog
F
Fortinet All Blogs
腾讯CDC
博客园 - Franky
WordPress大学
WordPress大学
U
Unit 42

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
refactor: migrate agent session accessors (#96182) · open...
jalehman · 2026-06-24 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -76,12 +76,15 @@ export const migratedSessionAccessorFiles = new Set([

7676

"packages/memory-host-sdk/src/host/session-files.ts",

7777

"src/acp/runtime/session-meta.ts",

7878

"src/agents/acp-spawn.ts",

79+

"src/agents/auth-profiles/session-override.ts",

7980

"src/agents/embedded-agent-runner/compaction-successor-transcript.ts",

8081

"src/agents/embedded-agent-runner/run/attempt.ts",

8182

"src/agents/embedded-agent-runner/tool-result-truncation.ts",

8283

"src/agents/embedded-agent-runner/transcript-rewrite.ts",

8384

"src/agents/embedded-agent-runner/transcript-runtime-state.ts",

8485

"src/auto-reply/reply/abort.ts",

86+

"src/agents/subagent-control.ts",

87+

"src/agents/subagent-registry-helpers.ts",

8588

"src/auto-reply/reply/agent-runner-helpers.ts",

8689

"src/auto-reply/reply/agent-runner.ts",

8790

"src/auto-reply/reply/commands-subagents/action-info.ts",

@@ -127,12 +130,16 @@ export const migratedBundledPluginSessionAccessorFiles = new Set([

127130
128131

export const migratedSessionAccessorWriteFiles = new Set([

129132

"src/acp/runtime/session-meta.ts",

133+

"src/agents/auth-profiles/session-override.ts",

130134

"src/agents/command/attempt-execution.shared.ts",

131135

"src/agents/command/session-store.ts",

132136

"src/agents/embedded-agent-runner/run.ts",

133137

"src/agents/embedded-agent-runner/run/attempt.ts",

138+

"src/agents/live-model-switch.ts",

134139

"src/agents/main-session-restart-recovery.ts",

135140

"src/auto-reply/reply/abort.ts",

141+

"src/agents/subagent-control.ts",

142+

"src/agents/subagent-registry-helpers.ts",

136143

"src/auto-reply/reply/abort-cutoff.runtime.ts",

137144

"src/auto-reply/reply/agent-runner-cli-dispatch.ts",

138145

"src/auto-reply/reply/agent-runner-execution.ts",

Original file line numberDiff line numberDiff line change

@@ -14,14 +14,14 @@ import {

1414

import { ensureAuthProfileStore, hasAnyAuthProfileStoreSource } from "../auth-profiles/store.js";

1515

import { isProfileInCooldown } from "../auth-profiles/usage.js";

1616
17-

const sessionStoreRuntimeLoader = createLazyImportLoader(

18-

() => import("../../config/sessions/store.runtime.js"),

17+

const sessionAccessorLoader = createLazyImportLoader(

18+

() => import("../../config/sessions/session-accessor.js"),

1919

);

2020
21-

// Session-store writes are lazy-loaded so read-only auth resolution paths do not

22-

// import persistence code unless an override must be updated.

23-

function loadSessionStoreRuntime() {

24-

return sessionStoreRuntimeLoader.load();

21+

// Session accessor writes are lazy-loaded so read-only auth resolution paths do

22+

// not import persistence code unless an override must be updated.

23+

function loadSessionAccessor() {

24+

return sessionAccessorLoader.load();

2525

}

2626
2727

// Current session overrides are only valid when the selected provider can use

@@ -83,10 +83,12 @@ export async function clearSessionAuthProfileOverride(params: {

8383

sessionStore[sessionKey] = sessionEntry;

8484

if (storePath) {

8585

await (

86-

await loadSessionStoreRuntime()

87-

).updateSessionStore(storePath, (store) => {

88-

store[sessionKey] = sessionEntry;

89-

});

86+

await loadSessionAccessor()

87+

).patchSessionEntry(

88+

{ storePath, sessionKey },

89+

() => sessionEntry,

90+

{ fallbackEntry: sessionEntry, replaceEntry: true },

91+

);

9092

}

9193

}

9294

@@ -236,10 +238,12 @@ export async function resolveSessionAuthProfileOverride(params: {

236238

sessionStore[sessionKey] = sessionEntry;

237239

if (storePath) {

238240

await (

239-

await loadSessionStoreRuntime()

240-

).updateSessionStore(storePath, (storeLocal) => {

241-

storeLocal[sessionKey] = sessionEntry;

242-

});

241+

await loadSessionAccessor()

242+

).patchSessionEntry(

243+

{ storePath, sessionKey },

244+

() => sessionEntry,

245+

{ fallbackEntry: sessionEntry, replaceEntry: true },

246+

);

243247

}

244248

}

245249
Original file line numberDiff line numberDiff line change

@@ -32,6 +32,14 @@ vi.mock("../config/sessions/store.js", () => ({

3232

updateSessionStore: (...args: unknown[]) => state.updateSessionStoreMock(...args),

3333

}));

3434
35+

vi.mock("../config/sessions/session-accessor.js", () => ({

36+

loadSessionEntry: (scope: { sessionKey: string }) => {

37+

const store = state.loadSessionStoreMock(scope) as Record<string, unknown> | undefined;

38+

return store?.[scope.sessionKey];

39+

},

40+

patchSessionEntry: (...args: unknown[]) => state.updateSessionStoreMock(...args),

41+

}));

42+
3543

vi.mock("../config/sessions/paths.js", () => ({

3644

resolveStorePath: (...args: unknown[]) => state.resolveStorePathMock(...args),

3745

}));

@@ -126,9 +134,29 @@ describe("live model switch", () => {

126134

state.updateSessionStoreMock

127135

.mockReset()

128136

.mockImplementation(

129-

async (_path: string, updater: (store: Record<string, unknown>) => void) => {

130-

const store: Record<string, unknown> = {};

131-

updater(store);

137+

async (

138+

scope: { sessionKey: string },

139+

updater: (

140+

entry: Record<string, unknown>,

141+

) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null,

142+

) => {

143+

const store = state.loadSessionStoreMock(scope) as Record<

144+

string,

145+

Record<string, unknown>

146+

>;

147+

const entry = store?.[scope.sessionKey];

148+

if (!entry) {

149+

return null;

150+

}

151+

const next = await updater(entry);

152+

if (!next) {

153+

return entry;

154+

}

155+

for (const key of Object.keys(entry)) {

156+

delete entry[key];

157+

}

158+

Object.assign(entry, next);

159+

return entry;

132160

},

133161

);

134162

});

@@ -473,12 +501,6 @@ describe("live model switch", () => {

473501

modelOverride: "claude-opus-4-6",

474502

};

475503

state.loadSessionStoreMock.mockReturnValue({ main: sessionEntry });

476-

state.updateSessionStoreMock.mockImplementation(

477-

async (_path: string, updater: (store: Record<string, unknown>) => void) => {

478-

const store: Record<string, typeof sessionEntry> = { main: sessionEntry };

479-

updater(store);

480-

},

481-

);

482504
483505

const { shouldSwitchToLiveModel } = await loadModule();

484506

@@ -538,12 +560,7 @@ describe("live model switch", () => {

538560
539561

it("deletes liveModelSwitchPending from the session entry", async () => {

540562

const sessionEntry = { liveModelSwitchPending: true, sessionId: "s-1" };

541-

state.updateSessionStoreMock.mockImplementation(

542-

async (_path: string, updater: (store: Record<string, unknown>) => void) => {

543-

const store: Record<string, typeof sessionEntry> = { main: sessionEntry };

544-

updater(store);

545-

},

546-

);

563+

state.loadSessionStoreMock.mockReturnValue({ main: sessionEntry });

547564
548565

const { clearLiveModelSwitchPending } = await loadModule();

549566
Original file line numberDiff line numberDiff line change

@@ -5,7 +5,8 @@ import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";

55

import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";

66
77

import { resolveStorePath } from "../config/sessions/paths.js";

8-

import { loadSessionStore, updateSessionStore } from "../config/sessions/store.js";

8+

import { patchSessionEntry } from "../config/sessions/session-accessor.js";

9+

import { loadSessionStore } from "../config/sessions/store.js";

910

import {

1011

normalizeStoredOverrideModel,

1112

resolveDefaultModelForAgent,

@@ -211,10 +212,13 @@ export async function clearLiveModelSwitchPending(params: {

211212

if (!storePath) {

212213

return;

213214

}

214-

await updateSessionStore(storePath, (store) => {

215-

const entry = store[sessionKey];

216-

if (entry) {

217-

delete entry.liveModelSwitchPending;

218-

}

219-

});

215+

await patchSessionEntry(

216+

{ storePath, sessionKey },

217+

(entry) => {

218+

const next = { ...entry };

219+

delete next.liveModelSwitchPending;

220+

return next;

221+

},

222+

{ replaceEntry: true },

223+

);

220224

}

Original file line numberDiff line numberDiff line change

@@ -4,6 +4,11 @@ import fs from "node:fs";

44

import os from "node:os";

55

import path from "node:path";

66

import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

7+

import type {

8+

SessionAccessScope,

9+

SessionEntryPatchContext,

10+

SessionEntryPatchOptions,

11+

} from "../config/sessions/session-accessor.js";

712

import type { SessionEntry } from "../config/sessions/types.js";

813

import type { OpenClawConfig } from "../config/types.openclaw.js";

914

import type { CallGatewayOptions } from "../gateway/call.js";

@@ -120,14 +125,33 @@ function setSubagentControlDepsForTest(

120125

testing.setDepsForTest({

121126

abortEmbeddedAgentRun: () => false,

122127

clearSessionQueues: () => ({ followupCleared: 0, laneCleared: 0, keys: [] }),

123-

updateSessionStore: async <T>(

124-

storePath: string,

125-

mutator: (store: Record<string, SessionEntry>) => Promise<T> | T,

128+

patchSessionEntry: async (

129+

scope: SessionAccessScope,

130+

patcher: (

131+

entry: SessionEntry,

132+

context: SessionEntryPatchContext,

133+

) => Promise<Partial<SessionEntry> | null> | Partial<SessionEntry> | null,

134+

options: SessionEntryPatchOptions = {},

126135

) => {

127-

const store = JSON.parse(fs.readFileSync(storePath, "utf-8")) as Record<string, SessionEntry>;

128-

const result = await mutator(store);

129-

fs.writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");

130-

return result;

136+

if (!scope.storePath) {

137+

return null;

138+

}

139+

const store = JSON.parse(fs.readFileSync(scope.storePath, "utf-8")) as Record<

140+

string,

141+

SessionEntry

142+

>;

143+

const entry = store[scope.sessionKey];

144+

if (!entry) {

145+

return null;

146+

}

147+

const patch = await patcher(entry, { existingEntry: { ...entry } });

148+

if (!patch) {

149+

return entry;

150+

}

151+

const next = options.replaceEntry ? (patch as SessionEntry) : { ...entry, ...patch };

152+

store[scope.sessionKey] = next;

153+

fs.writeFileSync(scope.storePath, JSON.stringify(store, null, 2), "utf-8");

154+

return next;

131155

},

132156

...overrides,

133157

});

@@ -639,7 +663,7 @@ describe("killSubagentRunAdmin", () => {

639663

});

640664
641665

setSubagentControlDepsForTest({

642-

updateSessionStore: async () => {

666+

patchSessionEntry: async () => {

643667

throw new Error("session store unavailable");

644668

},

645669

});

Original file line numberDiff line numberDiff line change

@@ -7,7 +7,7 @@ import crypto from "node:crypto";

77

import type { ClearSessionQueueResult } from "../auto-reply/reply/queue.js";

88

import { resolveSubagentLabel, sortSubagentRuns } from "../auto-reply/reply/subagents-utils.js";

99

import { resolveStorePath } from "../config/sessions/paths.js";

10-

import { loadSessionStore, updateSessionStore } from "../config/sessions/store.js";

10+

import { loadSessionEntry, patchSessionEntry } from "../config/sessions/session-accessor.js";

1111

import type { SessionEntry } from "../config/sessions/types.js";

1212

import type { OpenClawConfig } from "../config/types.openclaw.js";

1313

import { callGateway } from "../gateway/call.js";

@@ -50,18 +50,18 @@ const SUBAGENT_REPLY_HISTORY_LIMIT = 50;

5050

const steerRateLimit = new Map<string, number>();

5151
5252

type GatewayCaller = typeof callGateway;

53-

type UpdateSessionStore = typeof updateSessionStore;

53+

type PatchSessionEntry = typeof patchSessionEntry;

5454

type AbortEmbeddedAgentRun = (sessionId: string) => boolean;

5555

type ClearSessionQueues = (keys: Array<string | undefined>) => ClearSessionQueueResult;

5656
5757

const defaultSubagentControlDeps = {

5858

callGateway,

59-

updateSessionStore,

59+

patchSessionEntry,

6060

};

6161
6262

let subagentControlDeps: {

6363

callGateway: GatewayCaller;

64-

updateSessionStore: UpdateSessionStore;

64+

patchSessionEntry: PatchSessionEntry;

6565

abortEmbeddedAgentRun?: AbortEmbeddedAgentRun;

6666

clearSessionQueues?: ClearSessionQueues;

6767

} = defaultSubagentControlDeps;

@@ -187,15 +187,15 @@ async function killSubagentRun(params: {

187187

}

188188

if (resolved.entry) {

189189

try {

190-

await subagentControlDeps.updateSessionStore(resolved.storePath, (store) => {

191-

const current = store[childSessionKey];

192-

if (!current) {

193-

return;

194-

}

195-

current.abortedLastRun = true;

196-

current.updatedAt = Date.now();

197-

store[childSessionKey] = current;

198-

});

190+

await subagentControlDeps.patchSessionEntry(

191+

{ storePath: resolved.storePath, sessionKey: childSessionKey },

192+

(current) => ({

193+

...current,

194+

abortedLastRun: true,

195+

updatedAt: Date.now(),

196+

}),

197+

{ replaceEntry: true },

198+

);

199199

} catch (error) {

200200

logVerbose(

201201

`subagents control kill: failed to persist abortedLastRun for ${childSessionKey}: ${formatErrorMessage(error)}`,

@@ -660,8 +660,11 @@ export async function sendControlledSubagentMessage(params: {

660660

const targetSessionKey = params.entry.childSessionKey;

661661

const parsed = parseAgentSessionKey(targetSessionKey);

662662

const storePath = resolveStorePath(params.cfg.session?.store, { agentId: parsed?.agentId });

663-

const store = loadSessionStore(storePath);

664-

const targetSessionEntry = store[targetSessionKey];

663+

const targetSessionEntry = loadSessionEntry({

664+

storePath,

665+

sessionKey: targetSessionKey,

666+

clone: false,

667+

});

665668

const targetSessionId =

666669

typeof targetSessionEntry?.sessionId === "string" && targetSessionEntry.sessionId.trim()

667670

? targetSessionEntry.sessionId.trim()

@@ -724,7 +727,7 @@ export const testing = {

724727

setDepsForTest(

725728

overrides?: Partial<{

726729

callGateway: GatewayCaller;

727-

updateSessionStore: UpdateSessionStore;

730+

patchSessionEntry: PatchSessionEntry;

728731

abortEmbeddedAgentRun: AbortEmbeddedAgentRun;

729732

clearSessionQueues: ClearSessionQueues;

730733

}>,