Skip to content

Commit cb0d8a1

Browse files
authored
refactor(sessions): move inbound meta, goals, and delivery reads behind the session accessor (#101180)
1 parent bb658d0 commit cb0d8a1

20 files changed

Lines changed: 377 additions & 173 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
1805b1ddbd9036dac247b4eb01144c791e54e3a4826298bca4f85f67e936d8e2 plugin-sdk-api-baseline.json
2-
e1a4138ec4962ecb09472bf97cabebb98477d82af75f701142323706dcaa95f2 plugin-sdk-api-baseline.jsonl
1+
84f32cec516711bd2b96bcb0bf07f086ee63867a21b4d00de3fcca3def17070d plugin-sdk-api-baseline.json
2+
bd4505b690bbe4cad38712d2c6de15cfde905c3ed76bafad86b41f2028292f2b plugin-sdk-api-baseline.jsonl

scripts/check-session-accessor-boundary.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const legacyReaderNames = new Set([
1717
"readSessionEntries",
1818
"readSessionEntry",
1919
"readSessionStoreReadOnly",
20+
"readSessionStoreSnapshot",
2021
"resolveSessionStoreEntry",
2122
]);
2223
const legacyWholeStoreAccessNames = new Set([
@@ -26,7 +27,9 @@ const legacyWholeStoreAccessNames = new Set([
2627
]);
2728
const legacyWriterNames = new Set([
2829
"applySessionStoreEntryPatch",
30+
"recordSessionMetaFromInbound",
2931
"saveSessionStore",
32+
"updateLastRoute",
3033
"updateSessionStore",
3134
"updateSessionStoreEntry",
3235
]);
@@ -101,6 +104,8 @@ export const migratedSessionAccessorFiles = new Set([
101104
"src/commands/status.summary.ts",
102105
"src/commands/tasks.ts",
103106
"src/config/sessions/combined-store-gateway.ts",
107+
"src/config/sessions/delivery-info.ts",
108+
"src/config/sessions/goals.ts",
104109
"src/cron/isolated-agent/delivery-target.ts",
105110
"src/cron/service/timer.ts",
106111
"src/gateway/session-compaction-checkpoints.ts",
@@ -156,6 +161,7 @@ export const migratedSessionAccessorWriteFiles = new Set([
156161
"src/agents/command/attempt-execution.shared.ts",
157162
"src/agents/command/session-store.ts",
158163
"src/agents/embedded-agent-runner/run.ts",
164+
"src/agents/embedded-agent-subscribe.handlers.compaction.runtime.ts",
159165
"src/agents/embedded-agent-runner/run/attempt.ts",
160166
"src/agents/live-model-switch.ts",
161167
"src/agents/main-session-restart-recovery.ts",
@@ -185,12 +191,15 @@ export const migratedSessionAccessorWriteFiles = new Set([
185191
"src/auto-reply/reply/session-usage.ts",
186192
"src/commands/tasks.ts",
187193
"src/config/sessions/cleanup-service.ts",
194+
"src/config/sessions/goals.ts",
188195
"src/gateway/boot.ts",
189196
"src/gateway/server-methods/sessions.ts",
190197
"src/gateway/server-node-events.ts",
191198
"src/gateway/session-compaction-checkpoints.ts",
199+
"src/infra/outbound/outbound-session.ts",
192200
"src/plugins/host-hook-cleanup.ts",
193201
"src/plugins/host-hook-state.ts",
202+
"src/plugins/runtime/runtime-channel.ts",
194203
"src/tui/embedded-backend.ts",
195204
]);
196205

@@ -565,6 +574,7 @@ const writeSourceRootPaths = [
565574
"src/commands",
566575
"src/config/sessions",
567576
"src/gateway",
577+
"src/infra",
568578
"src/plugins",
569579
"src/tui",
570580
];

scripts/lib/session-accessor-debt-baseline.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"src/config/sessions/cleanup-service.ts": 4,
3434
"src/config/sessions/session-accessor.ts": 25,
3535
"src/config/sessions/session-registry-maintenance.ts": 2,
36-
"src/config/sessions/store-load.ts": 4,
36+
"src/config/sessions/store-load.ts": 5,
3737
"src/config/sessions/store.ts": 15,
3838
"src/config/sessions/test-helpers.ts": 2,
3939
"src/config/sessions/transcript.ts": 6,
@@ -43,7 +43,6 @@
4343
"src/plugins/runtime/runtime-agent.ts": 1
4444
},
4545
"sessionAccessorWrite": {
46-
"src/agents/embedded-agent-subscribe.handlers.compaction.runtime.ts": 2,
4746
"src/agents/session-suspension.ts": 2,
4847
"src/agents/subagent-orphan-recovery.ts": 3,
4948
"src/agents/subagent-spawn.ts": 2,
@@ -55,13 +54,15 @@
5554
"src/commands/doctor-state-integrity.ts": 2,
5655
"src/commands/doctor/shared/codex-route-warnings.ts": 2,
5756
"src/config/sessions/plugin-host-cleanup.ts": 2,
58-
"src/config/sessions/session-accessor.ts": 11,
57+
"src/config/sessions/session-accessor.ts": 13,
5958
"src/config/sessions/session-file.ts": 2,
6059
"src/config/sessions/session-registry-maintenance.ts": 2,
6160
"src/gateway/server-methods/agent.ts": 2,
6261
"src/gateway/server-methods/chat.ts": 2,
6362
"src/gateway/session-lifecycle-state.ts": 2,
6463
"src/gateway/test-helpers.mocks.ts": 1,
64+
"src/infra/heartbeat-runner.ts": 5,
65+
"src/infra/state-migrations.ts": 2,
6566
"src/plugins/runtime/runtime-agent.ts": 2
6667
},
6768
"sessionCompactManualTrim": {},
Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/**
22
* Runtime helpers for reconciling compaction counts after subscribe events.
33
*/
4-
import { resolveStorePath, updateSessionStoreEntry } from "../config/sessions.js";
4+
import { resolveStorePath } from "../config/sessions/paths.js";
5+
import { updateSessionEntry } from "../config/sessions/session-accessor.js";
56

67
/** Persist the highest observed compaction count after a successful subscribed run. */
78
export default async function reconcileSessionStoreCompactionCountAfterSuccess(params: {
@@ -16,22 +17,18 @@ export default async function reconcileSessionStoreCompactionCountAfterSuccess(p
1617
return undefined;
1718
}
1819
const storePath = resolveStorePath(configStore, { agentId });
19-
const nextEntry = await updateSessionStoreEntry({
20-
storePath,
21-
sessionKey,
22-
update: async (entry) => {
23-
// The live stream and store can both observe compactions. Keep the max so
24-
// late lower-count updates cannot make future resume labels regress.
25-
const currentCount = Math.max(0, entry.compactionCount ?? 0);
26-
const nextCount = Math.max(currentCount, observedCompactionCount);
27-
if (nextCount === currentCount) {
28-
return null;
29-
}
30-
return {
31-
compactionCount: nextCount,
32-
updatedAt: Math.max(entry.updatedAt ?? 0, now),
33-
};
34-
},
20+
const nextEntry = await updateSessionEntry({ sessionKey, storePath }, async (entry) => {
21+
// The live stream and store can both observe compactions. Keep the max so
22+
// late lower-count updates cannot make future resume labels regress.
23+
const currentCount = Math.max(0, entry.compactionCount ?? 0);
24+
const nextCount = Math.max(currentCount, observedCompactionCount);
25+
if (nextCount === currentCount) {
26+
return null;
27+
}
28+
return {
29+
compactionCount: nextCount,
30+
updatedAt: Math.max(entry.updatedAt ?? 0, now),
31+
};
3532
});
3633
return nextEntry?.compactionCount;
3734
}

src/channels/session-meta.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export async function recordInboundSessionMetaSafe(params: {
2323
agentId: params.agentId,
2424
});
2525
try {
26-
await runtime.recordSessionMetaFromInbound({
26+
await runtime.recordInboundSessionMeta({
2727
storePath,
2828
sessionKey: params.sessionKey,
2929
ctx: params.ctx,

src/channels/session.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ const recordSessionMetaFromInboundMock = vi.fn((_args?: unknown) => Promise.reso
66
const updateLastRouteMock = vi.fn((_args?: unknown) => Promise.resolve(undefined));
77

88
vi.mock("../config/sessions/inbound.runtime.js", () => ({
9-
recordSessionMetaFromInbound: (args: unknown) => recordSessionMetaFromInboundMock(args),
10-
updateLastRoute: (args: unknown) => updateLastRouteMock(args),
9+
recordInboundSessionMeta: (args: unknown) => recordSessionMetaFromInboundMock(args),
10+
updateSessionLastRoute: (args: unknown) => updateLastRouteMock(args),
1111
}));
1212

1313
type SessionModule = typeof import("./session.js");

src/channels/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export async function recordInboundSession(params: {
4343
const canonicalSessionKey = normalizeSessionKeyPreservingOpaquePeerIds(sessionKey);
4444
const runtime = await loadInboundSessionRuntime();
4545
const metaTask = runtime
46-
.recordSessionMetaFromInbound({
46+
.recordInboundSessionMeta({
4747
storePath,
4848
sessionKey: canonicalSessionKey,
4949
ctx,
@@ -62,7 +62,7 @@ export async function recordInboundSession(params: {
6262
return;
6363
}
6464
const targetSessionKey = normalizeSessionKeyPreservingOpaquePeerIds(update.sessionKey);
65-
await runtime.updateLastRoute({
65+
await runtime.updateSessionLastRoute({
6666
storePath,
6767
sessionKey: targetSessionKey,
6868
route: update.route,

src/config/sessions/delivery-info.test.ts

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ const storeState = vi.hoisted(() => {
99
const state = {
1010
store: {} as Record<string, SessionEntry>,
1111
stores: {} as Record<string, Record<string, SessionEntry>>,
12-
loadSessionStore: vi.fn((storePath: string) => state.stores[storePath] ?? state.store),
13-
readSessionStoreSnapshot: vi.fn((storePath: string) => state.stores[storePath] ?? state.store),
12+
// Mirrors the accessor view contract: raw exact-key get, enumeration only via entries().
13+
openSessionEntryReadView: vi.fn((scope: { storePath?: string }) => {
14+
const store = state.stores[scope.storePath ?? ""] ?? state.store;
15+
return {
16+
get: (sessionKey: string) =>
17+
Object.hasOwn(store, sessionKey) ? store[sessionKey] : undefined,
18+
entries: () => Object.entries(store).map(([sessionKey, entry]) => ({ sessionKey, entry })),
19+
};
20+
}),
1421
};
1522
return state;
1623
});
@@ -24,9 +31,8 @@ vi.mock("./paths.js", () => ({
2431
opts?.agentId === "worker" ? "/tmp/worker-sessions.json" : "/tmp/sessions.json",
2532
}));
2633

27-
vi.mock("./store.js", () => ({
28-
loadSessionStore: storeState.loadSessionStore,
29-
readSessionStoreSnapshot: storeState.readSessionStoreSnapshot,
34+
vi.mock("./session-accessor.js", () => ({
35+
openSessionEntryReadView: storeState.openSessionEntryReadView,
3036
}));
3137

3238
vi.mock("./targets.js", () => ({
@@ -53,8 +59,7 @@ beforeEach(() => {
5359
setActivePluginRegistry(createSessionConversationTestRegistry());
5460
storeState.store = {};
5561
storeState.stores = {};
56-
storeState.loadSessionStore.mockClear();
57-
storeState.readSessionStoreSnapshot.mockClear();
62+
storeState.openSessionEntryReadView.mockClear();
5863
});
5964

6065
describe("extractDeliveryInfo", () => {
@@ -94,7 +99,7 @@ describe("extractDeliveryInfo", () => {
9499
});
95100
});
96101

97-
it("uses session-store snapshots for direct session keys", () => {
102+
it("reads borrowed accessor views for direct session keys", () => {
98103
const sessionKey = "agent:main:webchat:dm:user-123";
99104
storeState.store[sessionKey] = buildEntry({
100105
channel: "webchat",
@@ -105,32 +110,16 @@ describe("extractDeliveryInfo", () => {
105110
const result = extractDeliveryInfo(sessionKey);
106111

107112
expect(result.deliveryContext?.to).toBe("webchat:user-123");
108-
expect(storeState.readSessionStoreSnapshot).toHaveBeenCalledWith("/tmp/sessions.json");
109-
expect(storeState.loadSessionStore).not.toHaveBeenCalled();
110-
});
111-
112-
it("returns deliveryContext for direct session keys", () => {
113-
const sessionKey = "agent:main:webchat:dm:user-123";
114-
storeState.store[sessionKey] = buildEntry({
115-
channel: "webchat",
116-
to: "webchat:user-123",
117-
accountId: "default",
118-
});
119-
120-
const result = extractDeliveryInfo(sessionKey);
121-
122-
expect(result).toEqual({
123-
deliveryContext: {
124-
channel: "webchat",
125-
to: "webchat:user-123",
126-
accountId: "default",
127-
},
128-
threadId: undefined,
113+
expect(storeState.openSessionEntryReadView).toHaveBeenCalledWith({
114+
storePath: "/tmp/sessions.json",
129115
});
130116
});
131117

132-
it("does not build the normalized index when an exact routable key is present", () => {
118+
it("does not enumerate the store when an exact routable key is present", () => {
133119
const sessionKey = "agent:main:webchat:dm:user-123";
120+
// Enumeration trap: the accessor-view mock lists rows via Object.entries, so
121+
// building the normalized fallback index on this cheap path throws here and
122+
// extractDeliveryInfo would return no delivery context.
134123
storeState.store = new Proxy(
135124
{
136125
[sessionKey]: buildEntry({
@@ -158,6 +147,26 @@ describe("extractDeliveryInfo", () => {
158147
});
159148
});
160149

150+
it("returns deliveryContext for direct session keys", () => {
151+
const sessionKey = "agent:main:webchat:dm:user-123";
152+
storeState.store[sessionKey] = buildEntry({
153+
channel: "webchat",
154+
to: "webchat:user-123",
155+
accountId: "default",
156+
});
157+
158+
const result = extractDeliveryInfo(sessionKey);
159+
160+
expect(result).toEqual({
161+
deliveryContext: {
162+
channel: "webchat",
163+
to: "webchat:user-123",
164+
accountId: "default",
165+
},
166+
threadId: undefined,
167+
});
168+
});
169+
161170
it("falls back to base sessions for :thread: keys", () => {
162171
const baseKey = "agent:main:slack:channel:C0123ABC";
163172
const threadKey = `${baseKey}:thread:1234567890.123456`;

0 commit comments

Comments
 (0)