Skip to content

Commit d94ec37

Browse files
authored
Merge pull request #183 from Kaguya-19/fix/mem-setting
fix(memory): inherit agent model and accept flat schedule fields in gateway config
2 parents 393e13a + bfc1257 commit d94ec37

11 files changed

Lines changed: 123 additions & 39 deletions

File tree

scripts/dev-launcher.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ async function main() {
122122
const child = spawn(
123123
'npm',
124124
['--workspace', 'ui', 'run', 'dev:concurrent'],
125-
{ cwd: repoRoot, env, stdio: 'inherit' },
125+
{ cwd: repoRoot, env, stdio: 'inherit', shell: true },
126126
);
127127

128128
const forward = (signal) => {

src/cli/createLocalGateway.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ class ProjectRuntimeRegistry {
583583
const memory = createEdgeClawMemoryProviderFromConfig({
584584
config: snapshot.config.memory,
585585
modelConfig: snapshot.config.model,
586+
agentModel: snapshot.config.agent.model.id,
586587
projectRoot,
587588
now: this.options.now,
588589
telemetry: this.options.telemetry,

src/context/DefaultContextRuntime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export type DefaultContextRuntimeOptions = {
9090
const DEFAULT_MAX_CONTEXT_TOKENS = 8192;
9191
const DEFAULT_TRUNCATE_FIRST_RATIO = 0.5;
9292
const DEFAULT_TRUNCATE_SECOND_RATIO = 0.25;
93-
const DEFAULT_MEMORY_RETRIEVAL_TIMEOUT_MS = 5_000;
93+
const DEFAULT_MEMORY_RETRIEVAL_TIMEOUT_MS = 30_000;
9494

9595
export class DefaultContextRuntime implements ContextRuntime {
9696
private readonly extension: ExtensionResolver;

src/context/memory/createEdgeClawMemoryProviderFromConfig.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import type { TelemetryClient } from "../../telemetry/index.js";
2525
export type CreateEdgeClawMemoryProviderOptions = {
2626
config: PilotMemoryConfig | undefined;
2727
modelConfig?: ModelConfig;
28+
/** Fallback model ref ("provider/model") when memory.model is not set. */
29+
agentModel?: string;
2830
projectRoot: string;
2931
/** Optional logger forwarded to the underlying service. */
3032
logger?: {
@@ -47,7 +49,7 @@ export function createEdgeClawMemoryProviderFromConfig(
4749
const workspaceDir = options.projectRoot;
4850
const rootDir = cfg.rootDir;
4951

50-
const llm = resolveMemoryLlm(cfg, options.modelConfig);
52+
const llm = resolveMemoryLlm(cfg, options.modelConfig, options.agentModel);
5153

5254
const service = new EdgeClawMemoryService({
5355
workspaceDir,
@@ -76,14 +78,16 @@ export function createEdgeClawMemoryProviderFromConfig(
7678
function resolveMemoryLlm(
7779
cfg: PilotMemoryConfig,
7880
modelConfig?: ModelConfig,
81+
agentModel?: string,
7982
): EdgeClawMemoryLlmOptions | undefined {
80-
if (!cfg.model) return undefined;
83+
const modelRef = cfg.model || agentModel;
84+
if (!modelRef) return undefined;
8185

82-
const sep = cfg.model.indexOf("/");
86+
const sep = modelRef.indexOf("/");
8387
if (sep < 0) return undefined;
8488

85-
const providerId = cfg.model.slice(0, sep);
86-
const modelId = cfg.model.slice(sep + 1);
89+
const providerId = modelRef.slice(0, sep);
90+
const modelId = modelRef.slice(sep + 1);
8791
const providerEntry = modelConfig?.providers[providerId];
8892

8993
const llm: EdgeClawMemoryLlmOptions = {

src/context/memory/edgeclaw-memory-core/lib/core/storage/sqlite.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
1+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
22
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
33
import { MEMORY_EXPORT_FORMAT_VERSION, } from "../types.js";
44
import { FileMemoryStore } from "../file-memory.js";
@@ -285,6 +285,25 @@ function createSiblingTempPath(targetDir, label) {
285285
const parentDir = dirname(targetDir);
286286
return join(parentDir, `.${basename(targetDir)}.${label}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
287287
}
288+
/**
289+
* Attempt `renameSync`; on Windows EPERM/EACCES (file-locking by indexer,
290+
* antivirus, or watchers), fall back to recursive copy + delete.
291+
*/
292+
function robustRenameSync(src, dst) {
293+
try {
294+
renameSync(src, dst);
295+
}
296+
catch (error) {
297+
const code = error.code;
298+
if (code === "EPERM" || code === "EACCES") {
299+
cpSync(src, dst, { recursive: true });
300+
rmSync(src, { recursive: true, force: true });
301+
}
302+
else {
303+
throw error;
304+
}
305+
}
306+
}
288307
function sortSnapshotFiles(files) {
289308
return [...files].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
290309
}
@@ -990,17 +1009,17 @@ export class MemoryRepository {
9901009
let movedLiveRoot = false;
9911010
try {
9921011
if (existsSync(liveRoot)) {
993-
renameSync(liveRoot, backupRoot);
1012+
robustRenameSync(liveRoot, backupRoot);
9941013
movedLiveRoot = true;
9951014
}
996-
renameSync(stagedRoot, liveRoot);
1015+
robustRenameSync(stagedRoot, liveRoot);
9971016
}
9981017
catch (error) {
9991018
if (existsSync(stagedRoot)) {
10001019
rmSync(stagedRoot, { recursive: true, force: true });
10011020
}
10021021
if (movedLiveRoot && !existsSync(liveRoot) && existsSync(backupRoot)) {
1003-
renameSync(backupRoot, liveRoot);
1022+
robustRenameSync(backupRoot, liveRoot);
10041023
}
10051024
throw error;
10061025
}

src/context/memory/edgeclaw-memory-core/src/core/storage/sqlite.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
1+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
22
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
33
import {
44
type CaseTraceRecord,
@@ -427,6 +427,24 @@ function createSiblingTempPath(targetDir: string, label: string): string {
427427
);
428428
}
429429

430+
/**
431+
* Attempt `renameSync`; on Windows EPERM/EACCES (file-locking by indexer,
432+
* antivirus, or watchers), fall back to recursive copy + delete.
433+
*/
434+
function robustRenameSync(src: string, dst: string): void {
435+
try {
436+
renameSync(src, dst);
437+
} catch (error: unknown) {
438+
const code = (error as NodeJS.ErrnoException).code;
439+
if (code === "EPERM" || code === "EACCES") {
440+
cpSync(src, dst, { recursive: true });
441+
rmSync(src, { recursive: true, force: true });
442+
} else {
443+
throw error;
444+
}
445+
}
446+
}
447+
430448
function sortSnapshotFiles(files: readonly MemorySnapshotFileRecord[]): MemorySnapshotFileRecord[] {
431449
return [...files].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
432450
}
@@ -1249,16 +1267,16 @@ export class MemoryRepository {
12491267
let movedLiveRoot = false;
12501268
try {
12511269
if (existsSync(liveRoot)) {
1252-
renameSync(liveRoot, backupRoot);
1270+
robustRenameSync(liveRoot, backupRoot);
12531271
movedLiveRoot = true;
12541272
}
1255-
renameSync(stagedRoot, liveRoot);
1273+
robustRenameSync(stagedRoot, liveRoot);
12561274
} catch (error) {
12571275
if (existsSync(stagedRoot)) {
12581276
rmSync(stagedRoot, { recursive: true, force: true });
12591277
}
12601278
if (movedLiveRoot && !existsSync(liveRoot) && existsSync(backupRoot)) {
1261-
renameSync(backupRoot, liveRoot);
1279+
robustRenameSync(backupRoot, liveRoot);
12621280
}
12631281
throw error;
12641282
}

src/cron/storage/CronTaskStore.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
1+
import { appendFile, copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
22
import { dirname } from "node:path";
33
import type { GatewayEvent } from "../../gateway/index.js";
44
import type { CronRunRecord, CronTask } from "../protocol/types.js";
@@ -120,7 +120,17 @@ export class CronTaskStore {
120120
await mkdir(dirname(this.paths.tasksFile), { recursive: true });
121121
const tempPath = `${this.paths.tasksFile}.${process.pid}.${Date.now()}.tmp`;
122122
await writeFile(tempPath, JSON.stringify(file, null, 2), "utf-8");
123-
await rename(tempPath, this.paths.tasksFile);
123+
try {
124+
await rename(tempPath, this.paths.tasksFile);
125+
} catch (err: unknown) {
126+
const code = (err as NodeJS.ErrnoException).code;
127+
if (code === "EPERM" || code === "EACCES") {
128+
await copyFile(tempPath, this.paths.tasksFile);
129+
await unlink(tempPath).catch(() => {});
130+
} else {
131+
throw err;
132+
}
133+
}
124134
}
125135
}
126136

src/pilot/config/parseMemoryConfig.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,17 @@ export function parseMemoryConfig(
4444
}
4545

4646
const memoryModel = parseMemoryModelRef(rawMemory.model, diagnostics, modelConfig);
47-
const schedule = parseMemorySchedule(rawMemory.schedule, diagnostics);
47+
const schedule = parseMemorySchedule(rawMemory.schedule, diagnostics)
48+
?? buildScheduleFromFlatFields(rawMemory);
4849

50+
const KNOWN_FIELDS = new Set([
51+
"enabled", "provider", "rootDir", "captureStrategy", "includeAssistant",
52+
"maxMessageChars", "retrievalTimeoutMs", "model", "apiType", "schedule",
53+
"heartbeatBatchSize",
54+
"reasoningMode", "autoIndexIntervalMinutes", "autoDreamIntervalMinutes",
55+
]);
4956
for (const key of Object.keys(rawMemory)) {
50-
if (
51-
key !== "enabled"
52-
&& key !== "provider"
53-
&& key !== "rootDir"
54-
&& key !== "captureStrategy"
55-
&& key !== "includeAssistant"
56-
&& key !== "maxMessageChars"
57-
&& key !== "retrievalTimeoutMs"
58-
&& key !== "model"
59-
&& key !== "apiType"
60-
&& key !== "schedule"
61-
&& key !== "heartbeatBatchSize"
62-
) {
57+
if (!KNOWN_FIELDS.has(key)) {
6358
diagnostics.push({
6459
code: "CONFIG_MEMORY_UNKNOWN_FIELD",
6560
severity: "warning",
@@ -129,15 +124,31 @@ function parseMemorySchedule(
129124
return Object.keys(schedule).length > 0 ? schedule : undefined;
130125
}
131126

127+
function buildScheduleFromFlatFields(rawMemory: Record<string, unknown>): PilotMemoryScheduleConfig | undefined {
128+
const schedule: PilotMemoryScheduleConfig = {};
129+
const reasoningMode = readOptionalMemoryReasoningMode(rawMemory.reasoningMode);
130+
if (reasoningMode !== undefined) schedule.reasoningMode = reasoningMode;
131+
if (typeof rawMemory.autoIndexIntervalMinutes === "number" && rawMemory.autoIndexIntervalMinutes >= 0) {
132+
schedule.autoIndexIntervalMinutes = rawMemory.autoIndexIntervalMinutes;
133+
}
134+
if (typeof rawMemory.autoDreamIntervalMinutes === "number" && rawMemory.autoDreamIntervalMinutes >= 0) {
135+
schedule.autoDreamIntervalMinutes = rawMemory.autoDreamIntervalMinutes;
136+
}
137+
return Object.keys(schedule).length > 0 ? schedule : undefined;
138+
}
139+
132140
function parseMemoryModelRef(
133141
value: unknown,
134142
diagnostics: PilotConfigDiagnostic[],
135143
modelConfig?: ModelConfig,
136144
): string | undefined {
137-
if (value === undefined) {
145+
if (value === undefined || value === null) {
138146
return undefined;
139147
}
140-
if (typeof value !== "string" || value.length === 0) {
148+
if (typeof value === "string" && value.trim().length === 0) {
149+
return undefined;
150+
}
151+
if (typeof value !== "string") {
141152
throw new PilotConfigError(
142153
"CONFIG_MEMORY_MODEL_INVALID",
143154
'memory.model must be a "provider/model" string.',

ui/server/routes/taskmaster.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,8 @@ router.get('/next/:projectName', async (req, res) => {
478478

479479
const nextTaskCommand = spawn('task-master', ['next'], {
480480
cwd: projectPath,
481-
stdio: ['pipe', 'pipe', 'pipe']
481+
stdio: ['pipe', 'pipe', 'pipe'],
482+
shell: true
482483
});
483484

484485
let stdout = '';
@@ -998,7 +999,8 @@ router.post('/init/:projectName', async (req, res) => {
998999
// Run taskmaster init command
9991000
const initProcess = spawn('npx', ['task-master', 'init'], {
10001001
cwd: projectPath,
1001-
stdio: ['pipe', 'pipe', 'pipe']
1002+
stdio: ['pipe', 'pipe', 'pipe'],
1003+
shell: true
10021004
});
10031005

10041006
let stdout = '';
@@ -1101,7 +1103,8 @@ router.post('/add-task/:projectName', async (req, res) => {
11011103
// Run task-master add-task command
11021104
const addTaskProcess = spawn('npx', args, {
11031105
cwd: projectPath,
1104-
stdio: ['pipe', 'pipe', 'pipe']
1106+
stdio: ['pipe', 'pipe', 'pipe'],
1107+
shell: true
11051108
});
11061109

11071110
let stdout = '';
@@ -1181,7 +1184,8 @@ router.put('/update-task/:projectName/:taskId', async (req, res) => {
11811184
if (status && Object.keys(req.body).length === 1) {
11821185
const setStatusProcess = spawn('npx', ['task-master-ai', 'set-status', `--id=${taskId}`, `--status=${status}`], {
11831186
cwd: projectPath,
1184-
stdio: ['pipe', 'pipe', 'pipe']
1187+
stdio: ['pipe', 'pipe', 'pipe'],
1188+
shell: true
11851189
});
11861190

11871191
let stdout = '';
@@ -1233,7 +1237,8 @@ router.put('/update-task/:projectName/:taskId', async (req, res) => {
12331237

12341238
const updateProcess = spawn('npx', ['task-master-ai', 'update-task', `--id=${taskId}`, `--prompt=${prompt}`], {
12351239
cwd: projectPath,
1236-
stdio: ['pipe', 'pipe', 'pipe']
1240+
stdio: ['pipe', 'pipe', 'pipe'],
1241+
shell: true
12371242
});
12381243

12391244
let stdout = '';
@@ -1332,7 +1337,8 @@ router.post('/parse-prd/:projectName', async (req, res) => {
13321337
// Run task-master parse-prd command
13331338
const parsePRDProcess = spawn('npx', args, {
13341339
cwd: projectPath,
1335-
stdio: ['pipe', 'pipe', 'pipe']
1340+
stdio: ['pipe', 'pipe', 'pipe'],
1341+
shell: true
13361342
});
13371343

13381344
let stdout = '';

ui/server/services/pilotdeckConfig.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,12 @@ export async function writePilotDeckConfig(config) {
560560
),
561561
),
562562
);
563+
if (isRecord(sanitized.memory)) {
564+
const memModel = sanitized.memory.model;
565+
if (typeof memModel === 'string' && !memModel.trim()) {
566+
delete sanitized.memory.model;
567+
}
568+
}
563569
const validation = validatePilotDeckConfig(sanitized);
564570
if (!validation.valid) {
565571
const error = new Error('Invalid PilotDeck config');
@@ -569,6 +575,12 @@ export async function writePilotDeckConfig(config) {
569575
const configPath = getPilotDeckConfigPath();
570576
await fsPromises.mkdir(path.dirname(configPath), { recursive: true });
571577
const yamlObj = validation.config;
578+
if (isRecord(yamlObj.memory)) {
579+
const memModel = yamlObj.memory.model;
580+
if (typeof memModel === 'string' && !memModel.trim()) {
581+
delete yamlObj.memory.model;
582+
}
583+
}
572584
const raw = stringifyYaml(yamlObj, { lineWidth: 0 });
573585
await fsPromises.writeFile(configPath, raw, 'utf8');
574586
return { configPath, raw, validation, config: yamlObj };

0 commit comments

Comments
 (0)