refactor: keep runtime context in memory
This commit is contained in:
@@ -1,13 +1,12 @@
|
|||||||
import { tool } from "@opencode-ai/plugin";
|
import { tool } from "@opencode-ai/plugin";
|
||||||
import { MemoryStore } from "../../src/memory/store.js";
|
import { MemoryStore } from "../../src/memory/store.js";
|
||||||
import { SessionRuntimeContextStore } from "../../src/sessions/runtimeContextStore.js";
|
import {
|
||||||
|
getRuntimeSessionContext,
|
||||||
|
setRuntimeSessionContext,
|
||||||
|
} from "../../src/runtime/sessionContext.js";
|
||||||
|
|
||||||
const memoryStore = new MemoryStore();
|
const memoryStore = new MemoryStore();
|
||||||
const toolContextStore = new SessionRuntimeContextStore();
|
const initializePromise = memoryStore.initialize();
|
||||||
const initializePromise = Promise.all([
|
|
||||||
memoryStore.initialize(),
|
|
||||||
toolContextStore.initialize(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description:
|
description:
|
||||||
@@ -37,7 +36,7 @@ export default tool({
|
|||||||
},
|
},
|
||||||
async execute(args, context) {
|
async execute(args, context) {
|
||||||
await initializePromise;
|
await initializePromise;
|
||||||
const sessionContext = await toolContextStore.read(context.sessionID);
|
const sessionContext = getRuntimeSessionContext(context.sessionID);
|
||||||
if (!sessionContext) {
|
if (!sessionContext) {
|
||||||
throw new Error(`session context not found for ${context.sessionID}`);
|
throw new Error(`session context not found for ${context.sessionID}`);
|
||||||
}
|
}
|
||||||
@@ -57,10 +56,10 @@ export default tool({
|
|||||||
}
|
}
|
||||||
if (sessionContext.allowLearningWrite === false && args.action !== "list") {
|
if (sessionContext.allowLearningWrite === false && args.action !== "list") {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
kind: "memory",
|
kind: "memory",
|
||||||
decision: "rejected",
|
decision: "rejected",
|
||||||
detail: "memory writes are disabled for this session",
|
detail: "memory writes are disabled for this session",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,17 +70,17 @@ export default tool({
|
|||||||
...(sessionContext.memoryListReadScopes ?? {}),
|
...(sessionContext.memoryListReadScopes ?? {}),
|
||||||
[scope]: true,
|
[scope]: true,
|
||||||
};
|
};
|
||||||
await toolContextStore.write({
|
setRuntimeSessionContext({
|
||||||
...sessionContext,
|
...sessionContext,
|
||||||
memoryListReadScopes: readScopes,
|
memoryListReadScopes: readScopes,
|
||||||
});
|
});
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
kind: "memory",
|
kind: "memory",
|
||||||
decision: "accepted",
|
decision: "accepted",
|
||||||
detail: "memory listed",
|
detail: "memory listed",
|
||||||
items: await memoryStore.list(scope, scopeKey),
|
items: await memoryStore.list(scope, scopeKey),
|
||||||
target: scope,
|
target: scope,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,46 +95,55 @@ export default tool({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const result = await memoryStore.upsert(scope, scopeKey, {
|
const result = await memoryStore.upsert(scope, scopeKey, {
|
||||||
content: args.content ?? "",
|
content: args.content ?? "",
|
||||||
sessionId: sessionContext.clientSessionId,
|
sessionId: sessionContext.clientSessionId,
|
||||||
source: "tool",
|
source: "tool",
|
||||||
traceId: sessionContext.traceId,
|
traceId: sessionContext.traceId,
|
||||||
});
|
});
|
||||||
if (!result.entry) {
|
if (!result.entry) {
|
||||||
|
return JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
kind: "memory",
|
||||||
|
decision: "rejected",
|
||||||
|
detail: "content rejected by persistence policy",
|
||||||
|
});
|
||||||
|
}
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
kind: "memory",
|
kind: "memory",
|
||||||
decision: "rejected",
|
decision: result.changed ? "accepted" : "deduped",
|
||||||
detail: "content rejected by persistence policy",
|
detail: result.detail,
|
||||||
});
|
entry: result.entry,
|
||||||
}
|
target: scope,
|
||||||
return JSON.stringify({
|
|
||||||
ok: true,
|
|
||||||
kind: "memory",
|
|
||||||
decision: result.changed ? "accepted" : "deduped",
|
|
||||||
detail: result.detail,
|
|
||||||
entry: result.entry,
|
|
||||||
target: scope,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.action === "replace") {
|
if (args.action === "replace") {
|
||||||
const result = await memoryStore.replace(scope, scopeKey, args.target_id ?? "", {
|
const result = await memoryStore.replace(
|
||||||
content: args.content ?? "",
|
scope,
|
||||||
sessionId: sessionContext.clientSessionId,
|
scopeKey,
|
||||||
source: "tool",
|
args.target_id ?? "",
|
||||||
traceId: sessionContext.traceId,
|
{
|
||||||
});
|
content: args.content ?? "",
|
||||||
|
sessionId: sessionContext.clientSessionId,
|
||||||
|
source: "tool",
|
||||||
|
traceId: sessionContext.traceId,
|
||||||
|
},
|
||||||
|
);
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
kind: "memory",
|
kind: "memory",
|
||||||
decision: result.changed ? "accepted" : "rejected",
|
decision: result.changed ? "accepted" : "rejected",
|
||||||
detail: result.detail,
|
detail: result.detail,
|
||||||
target: scope,
|
target: scope,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await memoryStore.remove(scope, scopeKey, args.target_id ?? "");
|
const result = await memoryStore.remove(
|
||||||
|
scope,
|
||||||
|
scopeKey,
|
||||||
|
args.target_id ?? "",
|
||||||
|
);
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
kind: "memory",
|
kind: "memory",
|
||||||
|
|||||||
+136
-105
@@ -1,123 +1,154 @@
|
|||||||
import { tool } from "@opencode-ai/plugin";
|
import { tool } from "@opencode-ai/plugin";
|
||||||
|
|
||||||
import { SkillStore } from "../../src/skills/store.js";
|
import { SkillStore } from "../../src/skills/store.js";
|
||||||
import { SessionRuntimeContextStore } from "../../src/sessions/runtimeContextStore.js";
|
import {
|
||||||
|
getRuntimeSessionContext,
|
||||||
|
type RuntimeSessionContext,
|
||||||
|
} from "../../src/runtime/sessionContext.js";
|
||||||
|
|
||||||
|
type ToolContextReader = {
|
||||||
|
read(sessionId: string): RuntimeSessionContext | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtimeContextReader: ToolContextReader = {
|
||||||
|
read: getRuntimeSessionContext,
|
||||||
|
};
|
||||||
|
|
||||||
export const createSkillManagerTool = (
|
export const createSkillManagerTool = (
|
||||||
skillStore = new SkillStore(),
|
skillStore = new SkillStore(),
|
||||||
toolContextStore = new SessionRuntimeContextStore(),
|
toolContextStore: ToolContextReader = runtimeContextReader,
|
||||||
initializePromise: Promise<unknown> = toolContextStore.initialize(),
|
initializePromise: Promise<unknown> = Promise.resolve(),
|
||||||
) => tool({
|
) =>
|
||||||
description:
|
tool({
|
||||||
"维护已验证、可复用、非敏感的 workflow 或方法模式。支持 list、write_skill、remove_skill、append_pattern、remove_pattern、write_reference、remove_reference、write_script、remove_script。",
|
description:
|
||||||
args: {
|
"维护已验证、可复用、非敏感的 workflow 或方法模式。支持 list、write_skill、remove_skill、append_pattern、remove_pattern、write_reference、remove_reference、write_script、remove_script。",
|
||||||
action: tool.schema
|
args: {
|
||||||
.enum([
|
action: tool.schema
|
||||||
"list",
|
.enum([
|
||||||
"write_skill",
|
"list",
|
||||||
"remove_skill",
|
"write_skill",
|
||||||
"append_pattern",
|
"remove_skill",
|
||||||
"remove_pattern",
|
"append_pattern",
|
||||||
"write_reference",
|
"remove_pattern",
|
||||||
"remove_reference",
|
"write_reference",
|
||||||
"write_script",
|
"remove_reference",
|
||||||
"remove_script",
|
"write_script",
|
||||||
])
|
"remove_script",
|
||||||
.describe("Skill maintenance operation."),
|
])
|
||||||
reason: tool.schema
|
.describe("Skill maintenance operation."),
|
||||||
.string()
|
reason: tool.schema
|
||||||
.describe(
|
.string()
|
||||||
"Why this skill maintenance action is justified for future reuse.",
|
.describe(
|
||||||
),
|
"Why this skill maintenance action is justified for future reuse.",
|
||||||
skill_path: tool.schema
|
),
|
||||||
.string()
|
skill_path: tool.schema
|
||||||
.describe(
|
.string()
|
||||||
"Target skill directory path relative to .opencode/skills. Use 'workflow' for the workflow index, or '__root__' for the root skills index.",
|
.describe(
|
||||||
),
|
"Target skill directory path relative to .opencode/skills. Use 'workflow' for the workflow index, or '__root__' for the root skills index.",
|
||||||
pattern: tool.schema
|
),
|
||||||
.string()
|
pattern: tool.schema
|
||||||
.optional()
|
.string()
|
||||||
.describe("Pattern text used by append_pattern."),
|
.optional()
|
||||||
target_id: tool.schema
|
.describe("Pattern text used by append_pattern."),
|
||||||
.string()
|
target_id: tool.schema
|
||||||
.optional()
|
.string()
|
||||||
.describe("Stable learned pattern id used by remove_pattern."),
|
.optional()
|
||||||
file_path: tool.schema
|
.describe("Stable learned pattern id used by remove_pattern."),
|
||||||
.string()
|
file_path: tool.schema
|
||||||
.optional()
|
.string()
|
||||||
.describe("Asset file path. For references use references/*.md; for scripts use scripts/*.py."),
|
.optional()
|
||||||
content: tool.schema
|
.describe(
|
||||||
.string()
|
"Asset file path. For references use references/*.md; for scripts use scripts/*.py.",
|
||||||
.optional()
|
),
|
||||||
.describe("Content used by write_skill, write_reference, or write_script."),
|
content: tool.schema
|
||||||
},
|
.string()
|
||||||
async execute(args, context) {
|
.optional()
|
||||||
await initializePromise;
|
.describe(
|
||||||
const sessionContext = await toolContextStore.read(context.sessionID);
|
"Content used by write_skill, write_reference, or write_script.",
|
||||||
if (!sessionContext) {
|
),
|
||||||
throw new Error(`session context not found for ${context.sessionID}`);
|
},
|
||||||
}
|
async execute(args, context) {
|
||||||
if (sessionContext.allowLearningWrite === false && args.action !== "list") {
|
await initializePromise;
|
||||||
return JSON.stringify({
|
const sessionContext = toolContextStore.read(context.sessionID);
|
||||||
ok: true,
|
if (!sessionContext) {
|
||||||
kind: "skill",
|
throw new Error(`session context not found for ${context.sessionID}`);
|
||||||
decision: "rejected",
|
}
|
||||||
detail: "skill writes are disabled for this session",
|
if (
|
||||||
});
|
sessionContext.allowLearningWrite === false &&
|
||||||
}
|
args.action !== "list"
|
||||||
if (args.action === "list") {
|
) {
|
||||||
const result = await skillStore.list(args.skill_path);
|
|
||||||
if (!result) {
|
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
kind: "skill",
|
kind: "skill",
|
||||||
decision: "rejected",
|
decision: "rejected",
|
||||||
detail:
|
detail: "skill writes are disabled for this session",
|
||||||
"invalid skill_path; expected a relative path under .opencode/skills",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (args.action === "list") {
|
||||||
|
const result = await skillStore.list(args.skill_path);
|
||||||
|
if (!result) {
|
||||||
|
return JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
kind: "skill",
|
||||||
|
decision: "rejected",
|
||||||
|
detail:
|
||||||
|
"invalid skill_path; expected a relative path under .opencode/skills",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
kind: "skill",
|
||||||
|
decision: "accepted",
|
||||||
|
detail: "skill listed",
|
||||||
|
...result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result =
|
||||||
|
args.action === "write_skill"
|
||||||
|
? await skillStore.writeSkill(args.skill_path, args.content ?? "")
|
||||||
|
: args.action === "remove_skill"
|
||||||
|
? await skillStore.removeSkill(args.skill_path)
|
||||||
|
: args.action === "append_pattern"
|
||||||
|
? await skillStore.appendPattern(
|
||||||
|
args.skill_path,
|
||||||
|
args.pattern ?? "",
|
||||||
|
)
|
||||||
|
: args.action === "remove_pattern"
|
||||||
|
? await skillStore.removePattern(
|
||||||
|
args.skill_path,
|
||||||
|
args.target_id ?? "",
|
||||||
|
)
|
||||||
|
: args.action === "write_reference"
|
||||||
|
? await skillStore.writeReference(
|
||||||
|
args.skill_path,
|
||||||
|
args.file_path ?? "",
|
||||||
|
args.content ?? "",
|
||||||
|
)
|
||||||
|
: args.action === "remove_reference"
|
||||||
|
? await skillStore.removeReference(
|
||||||
|
args.skill_path,
|
||||||
|
args.file_path ?? "",
|
||||||
|
)
|
||||||
|
: args.action === "write_script"
|
||||||
|
? await skillStore.writeScript(
|
||||||
|
args.skill_path,
|
||||||
|
args.file_path ?? "",
|
||||||
|
args.content ?? "",
|
||||||
|
)
|
||||||
|
: await skillStore.removeScript(
|
||||||
|
args.skill_path,
|
||||||
|
args.file_path ?? "",
|
||||||
|
);
|
||||||
|
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
kind: "skill",
|
kind: "skill",
|
||||||
decision: "accepted",
|
decision: result.changed ? "accepted" : "rejected",
|
||||||
detail: "skill listed",
|
detail: result.detail,
|
||||||
...result,
|
target: result.target,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
});
|
||||||
const result =
|
|
||||||
args.action === "write_skill"
|
|
||||||
? await skillStore.writeSkill(args.skill_path, args.content ?? "")
|
|
||||||
: args.action === "remove_skill"
|
|
||||||
? await skillStore.removeSkill(args.skill_path)
|
|
||||||
: args.action === "append_pattern"
|
|
||||||
? await skillStore.appendPattern(args.skill_path, args.pattern ?? "")
|
|
||||||
: args.action === "remove_pattern"
|
|
||||||
? await skillStore.removePattern(args.skill_path, args.target_id ?? "")
|
|
||||||
: args.action === "write_reference"
|
|
||||||
? await skillStore.writeReference(
|
|
||||||
args.skill_path,
|
|
||||||
args.file_path ?? "",
|
|
||||||
args.content ?? "",
|
|
||||||
)
|
|
||||||
: args.action === "remove_reference"
|
|
||||||
? await skillStore.removeReference(args.skill_path, args.file_path ?? "")
|
|
||||||
: args.action === "write_script"
|
|
||||||
? await skillStore.writeScript(
|
|
||||||
args.skill_path,
|
|
||||||
args.file_path ?? "",
|
|
||||||
args.content ?? "",
|
|
||||||
)
|
|
||||||
: await skillStore.removeScript(args.skill_path, args.file_path ?? "");
|
|
||||||
|
|
||||||
return JSON.stringify({
|
|
||||||
ok: true,
|
|
||||||
kind: "skill",
|
|
||||||
decision: result.changed ? "accepted" : "rejected",
|
|
||||||
detail: result.detail,
|
|
||||||
target: result.target,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default createSkillManagerTool();
|
export default createSkillManagerTool();
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { randomUUID } from "node:crypto";
|
|||||||
|
|
||||||
import { logger } from "../logger.js";
|
import { logger } from "../logger.js";
|
||||||
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
||||||
import { SessionRuntimeContextStore } from "../sessions/runtimeContextStore.js";
|
import {
|
||||||
|
removeRuntimeSessionContext,
|
||||||
|
setRuntimeSessionContext,
|
||||||
|
} from "../runtime/sessionContext.js";
|
||||||
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
|
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
|
||||||
|
|
||||||
export type SessionBinding = {
|
export type SessionBinding = {
|
||||||
@@ -26,7 +29,6 @@ export type ChatRequestContext = SessionContext & {
|
|||||||
|
|
||||||
export class ChatSessionBridge {
|
export class ChatSessionBridge {
|
||||||
private readonly abortControllers = new Map<string, AbortController>();
|
private readonly abortControllers = new Map<string, AbortController>();
|
||||||
private readonly sessionRuntimeContextStore = new SessionRuntimeContextStore();
|
|
||||||
|
|
||||||
constructor(private readonly runtime: OpencodeRuntimeAdapter) {}
|
constructor(private readonly runtime: OpencodeRuntimeAdapter) {}
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ export class ChatSessionBridge {
|
|||||||
sessionId,
|
sessionId,
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
};
|
};
|
||||||
await this.sessionRuntimeContextStore.write({
|
setRuntimeSessionContext({
|
||||||
accessToken: requestContext.accessToken,
|
accessToken: requestContext.accessToken,
|
||||||
actorKey: requestContext.actorKey,
|
actorKey: requestContext.actorKey,
|
||||||
allowLearningWrite: true,
|
allowLearningWrite: true,
|
||||||
@@ -133,9 +135,7 @@ export class ChatSessionBridge {
|
|||||||
"failed while waiting for runtime session to become idle",
|
"failed while waiting for runtime session to become idle",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await this.sessionRuntimeContextStore.remove(sessionId).catch((error) => {
|
removeRuntimeSessionContext(sessionId);
|
||||||
logger.debug({ sessionId, err: error }, "failed to cleanup runtime tool context");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildRequestContext(context: {
|
private buildRequestContext(context: {
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ const envSchema = z
|
|||||||
OPENCODE_SKILLS_ROOT_DIR: z.string().default("./.opencode/skills"),
|
OPENCODE_SKILLS_ROOT_DIR: z.string().default("./.opencode/skills"),
|
||||||
// client 模式下,目标 opencode server 的基础地址。
|
// client 模式下,目标 opencode server 的基础地址。
|
||||||
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
|
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
|
||||||
// 提供给本地 opencode tools 读取的会话上下文目录。
|
|
||||||
SESSION_RUNTIME_CONTEXT_STORAGE_DIR: z.string().default("./data/session-runtime-contexts"),
|
|
||||||
// tjwater-cli 可执行文件路径。
|
// tjwater-cli 可执行文件路径。
|
||||||
TJWATER_CLI_PATH: z.string().default("./cli/tjwater-cli"),
|
TJWATER_CLI_PATH: z.string().default("./cli/tjwater-cli"),
|
||||||
// TJWater 后端 API 的基础地址。
|
// TJWater 后端 API 的基础地址。
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { SessionLearningStateStore } from "./sessionStateStore.js";
|
|||||||
import { MemoryStore, type MemoryScope } from "../memory/store.js";
|
import { MemoryStore, type MemoryScope } from "../memory/store.js";
|
||||||
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
||||||
import { SkillStore } from "../skills/store.js";
|
import { SkillStore } from "../skills/store.js";
|
||||||
import { SessionRuntimeContextStore } from "../sessions/runtimeContextStore.js";
|
import {
|
||||||
|
removeRuntimeSessionContext,
|
||||||
|
setRuntimeSessionContext,
|
||||||
|
} from "../runtime/sessionContext.js";
|
||||||
import {
|
import {
|
||||||
sanitizePersistentDocument,
|
sanitizePersistentDocument,
|
||||||
sanitizePersistentLine,
|
sanitizePersistentLine,
|
||||||
@@ -76,7 +79,6 @@ export class LearningOrchestrator {
|
|||||||
private readonly activeReviews = new Set<string>();
|
private readonly activeReviews = new Set<string>();
|
||||||
private readonly sessionLearningStateStore = new SessionLearningStateStore();
|
private readonly sessionLearningStateStore = new SessionLearningStateStore();
|
||||||
private readonly skillStore = new SkillStore();
|
private readonly skillStore = new SkillStore();
|
||||||
private readonly sessionRuntimeContextStore = new SessionRuntimeContextStore();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly runtime: OpencodeRuntimeAdapter,
|
private readonly runtime: OpencodeRuntimeAdapter,
|
||||||
@@ -85,10 +87,7 @@ export class LearningOrchestrator {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
await Promise.all([
|
await this.sessionLearningStateStore.initialize();
|
||||||
this.sessionLearningStateStore.initialize(),
|
|
||||||
this.sessionRuntimeContextStore.initialize(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onTurnCompleted(input: TurnReviewInput) {
|
async onTurnCompleted(input: TurnReviewInput) {
|
||||||
@@ -147,7 +146,7 @@ export class LearningOrchestrator {
|
|||||||
`learning-gate-${input.requestContext.clientSessionId}`,
|
`learning-gate-${input.requestContext.clientSessionId}`,
|
||||||
);
|
);
|
||||||
gateSessionId = gateSession.id;
|
gateSessionId = gateSession.id;
|
||||||
await this.sessionRuntimeContextStore.write({
|
setRuntimeSessionContext({
|
||||||
actorKey: input.requestContext.actorKey,
|
actorKey: input.requestContext.actorKey,
|
||||||
allowLearningWrite: false,
|
allowLearningWrite: false,
|
||||||
clientSessionId: `gate-${input.requestContext.clientSessionId}`,
|
clientSessionId: `gate-${input.requestContext.clientSessionId}`,
|
||||||
@@ -215,7 +214,7 @@ export class LearningOrchestrator {
|
|||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (gateSessionId) {
|
if (gateSessionId) {
|
||||||
await this.sessionRuntimeContextStore.remove(gateSessionId).catch(() => undefined);
|
removeRuntimeSessionContext(gateSessionId);
|
||||||
await this.runtime.abortSession(gateSessionId).catch(() => undefined);
|
await this.runtime.abortSession(gateSessionId).catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,7 +234,7 @@ export class LearningOrchestrator {
|
|||||||
const reviewSession = await this.runtime.createSession(
|
const reviewSession = await this.runtime.createSession(
|
||||||
`learning-review-${input.requestContext.clientSessionId}`,
|
`learning-review-${input.requestContext.clientSessionId}`,
|
||||||
);
|
);
|
||||||
await this.sessionRuntimeContextStore.write({
|
setRuntimeSessionContext({
|
||||||
actorKey: input.requestContext.actorKey,
|
actorKey: input.requestContext.actorKey,
|
||||||
allowLearningWrite: false,
|
allowLearningWrite: false,
|
||||||
clientSessionId: `review-${input.requestContext.clientSessionId}`,
|
clientSessionId: `review-${input.requestContext.clientSessionId}`,
|
||||||
@@ -283,7 +282,7 @@ export class LearningOrchestrator {
|
|||||||
traceId: input.requestContext.traceId,
|
traceId: input.requestContext.traceId,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
await this.sessionRuntimeContextStore.remove(reviewSession.id).catch(() => undefined);
|
removeRuntimeSessionContext(reviewSession.id);
|
||||||
await this.runtime.abortSession(reviewSession.id).catch(() => undefined);
|
await this.runtime.abortSession(reviewSession.id).catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export type RuntimeSessionContext = {
|
||||||
|
accessToken?: string;
|
||||||
|
actorKey: string;
|
||||||
|
allowLearningWrite?: boolean;
|
||||||
|
clientSessionId: string;
|
||||||
|
learningMode?: "interactive" | "review";
|
||||||
|
memoryListReadScopes?: Partial<Record<"user" | "workspace", boolean>>;
|
||||||
|
projectId?: string;
|
||||||
|
projectKey: string;
|
||||||
|
sessionId: string;
|
||||||
|
traceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contexts = new Map<string, RuntimeSessionContext>();
|
||||||
|
|
||||||
|
export const setRuntimeSessionContext = (context: RuntimeSessionContext) => {
|
||||||
|
contexts.set(context.sessionId, { ...context });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRuntimeSessionContext = (sessionId: string) => {
|
||||||
|
const context = contexts.get(sessionId);
|
||||||
|
return context ? { ...context } : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeRuntimeSessionContext = (sessionId: string) => {
|
||||||
|
contexts.delete(sessionId);
|
||||||
|
};
|
||||||
+4
-6
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "./results/store.js";
|
} from "./results/store.js";
|
||||||
import { buildChatRouter } from "./routes/chat.js";
|
import { buildChatRouter } from "./routes/chat.js";
|
||||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||||
import { SessionRuntimeContextStore } from "./sessions/runtimeContextStore.js";
|
import { getRuntimeSessionContext } from "./runtime/sessionContext.js";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@ const sessionMetadataStore = new SessionMetadataStore();
|
|||||||
const sessionUiStateStore = new SessionUiStateStore();
|
const sessionUiStateStore = new SessionUiStateStore();
|
||||||
const memoryStore = new MemoryStore();
|
const memoryStore = new MemoryStore();
|
||||||
const sessionTranscriptStore = new SessionTranscriptStore();
|
const sessionTranscriptStore = new SessionTranscriptStore();
|
||||||
const sessionRuntimeContextStore = new SessionRuntimeContextStore();
|
|
||||||
const learningOrchestrator = new LearningOrchestrator(
|
const learningOrchestrator = new LearningOrchestrator(
|
||||||
opencodeRuntime,
|
opencodeRuntime,
|
||||||
memoryStore,
|
memoryStore,
|
||||||
@@ -71,7 +70,7 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
|
|||||||
|
|
||||||
const sessionId =
|
const sessionId =
|
||||||
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
||||||
const context = sessionId ? await sessionRuntimeContextStore.read(sessionId) : null;
|
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
|
||||||
if (!context) {
|
if (!context) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
message: "session context not found",
|
message: "session context not found",
|
||||||
@@ -175,7 +174,7 @@ app.post("/internal/tools/store-render-ref", async (req, res) => {
|
|||||||
const sessionId =
|
const sessionId =
|
||||||
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
||||||
const filePath = typeof req.body?.file_path === "string" ? req.body.file_path.trim() : "";
|
const filePath = typeof req.body?.file_path === "string" ? req.body.file_path.trim() : "";
|
||||||
const context = sessionId ? await sessionRuntimeContextStore.read(sessionId) : null;
|
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
|
||||||
if (!context) {
|
if (!context) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
message: "session context not found",
|
message: "session context not found",
|
||||||
@@ -225,7 +224,7 @@ app.post("/internal/tools/session-search", async (req, res) => {
|
|||||||
const sessionId =
|
const sessionId =
|
||||||
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
|
||||||
const query = typeof req.body?.query === "string" ? req.body.query : "";
|
const query = typeof req.body?.query === "string" ? req.body.query : "";
|
||||||
const context = sessionId ? await sessionRuntimeContextStore.read(sessionId) : null;
|
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
|
||||||
if (!context) {
|
if (!context) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
message: "session context not found",
|
message: "session context not found",
|
||||||
@@ -273,7 +272,6 @@ const bootstrap = async () => {
|
|||||||
memoryStore.initialize(),
|
memoryStore.initialize(),
|
||||||
resultReferenceStore.initialize(),
|
resultReferenceStore.initialize(),
|
||||||
sessionTranscriptStore.initialize(),
|
sessionTranscriptStore.initialize(),
|
||||||
sessionRuntimeContextStore.initialize(),
|
|
||||||
]);
|
]);
|
||||||
resultReferenceStore.startCleanupLoop();
|
resultReferenceStore.startCleanupLoop();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
import { config } from "../config.js";
|
|
||||||
import {
|
|
||||||
atomicWriteJson,
|
|
||||||
ensureDirectory,
|
|
||||||
readJsonFile,
|
|
||||||
removeFileIfExists,
|
|
||||||
} from "../utils/fileStore.js";
|
|
||||||
|
|
||||||
export type SessionRuntimeContext = {
|
|
||||||
accessToken?: string;
|
|
||||||
actorKey: string;
|
|
||||||
allowLearningWrite?: boolean;
|
|
||||||
clientSessionId: string;
|
|
||||||
learningMode?: "interactive" | "review";
|
|
||||||
memoryListReadScopes?: Partial<Record<"user" | "workspace", boolean>>;
|
|
||||||
projectId?: string;
|
|
||||||
projectKey: string;
|
|
||||||
sessionId: string;
|
|
||||||
traceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class SessionRuntimeContextStore {
|
|
||||||
constructor(private readonly baseDir = config.SESSION_RUNTIME_CONTEXT_STORAGE_DIR) {}
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
await ensureDirectory(this.baseDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
async write(context: SessionRuntimeContext) {
|
|
||||||
await atomicWriteJson(this.filePath(context.sessionId), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
async read(sessionId: string) {
|
|
||||||
return await readJsonFile<SessionRuntimeContext>(this.filePath(sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(sessionId: string) {
|
|
||||||
await removeFileIfExists(this.filePath(sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private filePath(sessionId: string) {
|
|
||||||
return join(this.baseDir, `${sessionId}.json`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,13 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { createSkillManagerTool } from "../../.opencode/tools/skill_manager.js";
|
import { createSkillManagerTool } from "../../.opencode/tools/skill_manager.js";
|
||||||
import { SessionRuntimeContextStore } from "../../src/sessions/runtimeContextStore.js";
|
import { type RuntimeSessionContext } from "../../src/runtime/sessionContext.js";
|
||||||
import { SkillStore } from "../../src/skills/store.js";
|
import { SkillStore } from "../../src/skills/store.js";
|
||||||
|
|
||||||
describe("skill_manager tool", () => {
|
describe("skill_manager tool", () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
let skillStore: SkillStore;
|
let skillStore: SkillStore;
|
||||||
let contextStore: SessionRuntimeContextStore;
|
let context: RuntimeSessionContext;
|
||||||
|
|
||||||
const toolContext = {
|
const toolContext = {
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
@@ -39,16 +39,14 @@ describe("skill_manager tool", () => {
|
|||||||
join(tempDir, "skills"),
|
join(tempDir, "skills"),
|
||||||
join(tempDir, "backup", "skills"),
|
join(tempDir, "backup", "skills"),
|
||||||
);
|
);
|
||||||
contextStore = new SessionRuntimeContextStore(join(tempDir, "contexts"));
|
context = {
|
||||||
await contextStore.initialize();
|
|
||||||
await contextStore.write({
|
|
||||||
actorKey: "actor-1",
|
actorKey: "actor-1",
|
||||||
allowLearningWrite: true,
|
allowLearningWrite: true,
|
||||||
clientSessionId: "client-session-1",
|
clientSessionId: "client-session-1",
|
||||||
projectKey: "project-1",
|
projectKey: "project-1",
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
traceId: "trace-1",
|
traceId: "trace-1",
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -58,7 +56,7 @@ describe("skill_manager tool", () => {
|
|||||||
it("dispatches skill-level write, overwrite, and remove actions", async () => {
|
it("dispatches skill-level write, overwrite, and remove actions", async () => {
|
||||||
const tool = createSkillManagerTool(
|
const tool = createSkillManagerTool(
|
||||||
skillStore,
|
skillStore,
|
||||||
contextStore,
|
{ read: () => context },
|
||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -111,7 +109,7 @@ describe("skill_manager tool", () => {
|
|||||||
it("writes the root skills index through the reserved alias", async () => {
|
it("writes the root skills index through the reserved alias", async () => {
|
||||||
const tool = createSkillManagerTool(
|
const tool = createSkillManagerTool(
|
||||||
skillStore,
|
skillStore,
|
||||||
contextStore,
|
{ read: () => context },
|
||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getRuntimeSessionContext,
|
||||||
|
removeRuntimeSessionContext,
|
||||||
|
setRuntimeSessionContext,
|
||||||
|
} from "../../src/runtime/sessionContext.js";
|
||||||
|
|
||||||
|
describe("runtime session context", () => {
|
||||||
|
it("stores authentication context in process memory", () => {
|
||||||
|
setRuntimeSessionContext({
|
||||||
|
accessToken: "token-1",
|
||||||
|
actorKey: "actor-1",
|
||||||
|
allowLearningWrite: true,
|
||||||
|
clientSessionId: "chat-session-1",
|
||||||
|
learningMode: "interactive",
|
||||||
|
projectId: "project-id-1",
|
||||||
|
projectKey: "project-1",
|
||||||
|
sessionId: "runtime-session-1",
|
||||||
|
traceId: "trace-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtimeContext = getRuntimeSessionContext("runtime-session-1");
|
||||||
|
|
||||||
|
expect(runtimeContext?.accessToken).toBe("token-1");
|
||||||
|
expect(runtimeContext?.clientSessionId).toBe("chat-session-1");
|
||||||
|
expect(runtimeContext?.sessionId).toBe("runtime-session-1");
|
||||||
|
|
||||||
|
removeRuntimeSessionContext("runtime-session-1");
|
||||||
|
expect(getRuntimeSessionContext("runtime-session-1")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
||||||
import { mkdtemp, rm } from "node:fs/promises";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
import { SessionRuntimeContextStore } from "../../src/sessions/runtimeContextStore.js";
|
|
||||||
|
|
||||||
describe("SessionRuntimeContextStore", () => {
|
|
||||||
let tempDir: string;
|
|
||||||
let store: SessionRuntimeContextStore;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
tempDir = await mkdtemp(join(tmpdir(), "tjwater-tool-context-"));
|
|
||||||
store = new SessionRuntimeContextStore(tempDir);
|
|
||||||
await store.initialize();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await rm(tempDir, { force: true, recursive: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("writes and reads runtime session context by opencode session id", async () => {
|
|
||||||
await store.write({
|
|
||||||
accessToken: "token-1",
|
|
||||||
actorKey: "actor-1",
|
|
||||||
allowLearningWrite: true,
|
|
||||||
clientSessionId: "chat-session-1",
|
|
||||||
learningMode: "interactive",
|
|
||||||
projectId: "project-id-1",
|
|
||||||
projectKey: "project-1",
|
|
||||||
sessionId: "runtime-session-1",
|
|
||||||
traceId: "trace-1",
|
|
||||||
});
|
|
||||||
|
|
||||||
const runtimeContext = await store.read("runtime-session-1");
|
|
||||||
|
|
||||||
expect(runtimeContext?.accessToken).toBe("token-1");
|
|
||||||
expect(runtimeContext?.clientSessionId).toBe("chat-session-1");
|
|
||||||
expect(runtimeContext?.sessionId).toBe("runtime-session-1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user