@nowcrew/daemon 0.6.87-beta.1 → 0.6.87-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/automatic-install-target.js +7 -8
- package/dist/execution-runner.js +4 -2
- package/dist/local-executor.js +31 -3
- package/dist/local-memory/pending.js +136 -2
- package/dist/local-memory/runtime.js +8 -1
- package/dist/local-memory/write.js +126 -4
- package/dist/main.js +0 -0
- package/dist/project-skills/capability.js +109 -0
- package/dist/project-skills/controller-convergence.js +57 -0
- package/dist/project-skills/projection-set-switch.js +419 -0
- package/dist/project-skills/projection-state-domain.js +170 -0
- package/dist/project-skills/projection-state-store.js +842 -0
- package/dist/project-skills/projection-state-transaction.js +322 -0
- package/dist/project-skills/projection-state.js +3 -0
- package/dist/project-skills/runtime-root-bootstrap.js +47 -0
- package/dist/project-skills/runtime-root-domain.js +268 -0
- package/dist/project-skills/runtime-root-gc.js +293 -0
- package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
- package/dist/project-skills/runtime-root-leases.js +487 -0
- package/dist/project-skills/runtime-root-source-identity.js +60 -0
- package/dist/project-skills/runtime-root-startup.js +49 -0
- package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
- package/dist/project-skills/runtime-root-state-index.js +356 -0
- package/dist/project-skills/runtime-root-store.js +722 -0
- package/dist/project-skills/serve-capability.js +28 -0
- package/dist/project-skills/serve-startup.js +37 -0
- package/dist/prompt.js +4 -1
- package/dist/runner.js +3 -1
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -12,10 +12,12 @@ npx @nowcrew/daemon@latest --server-url https://nowwork.example --api-key sk_mac
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
The package has two release channels. The `in` environment consumes stable `x.y.z` releases through
|
|
15
|
-
the `latest` tag
|
|
15
|
+
the `latest` tag. The `dev` environment prefers `x.y.z-beta.n` prereleases through the `beta` tag,
|
|
16
|
+
and also accepts stable `x.y.z` releases for compatibility and cross-channel updates.
|
|
16
17
|
The parser also accepts the historical `x.y.z-beta-n` spelling and normalizes it for ordering; new
|
|
17
18
|
published versions should use the dotted SemVer form.
|
|
18
|
-
|
|
19
|
+
Use `beta` by default for dev installations; an explicit `latest` target is also supported there.
|
|
20
|
+
Never use `beta` for an in installation. See the
|
|
19
21
|
[Daemon Release Runbook](../docs/releases/daemon-release-runbook.md) for release and rollback procedures.
|
|
20
22
|
|
|
21
23
|
This compatibility form stays resident in the current terminal and reconnects with backoff. New
|
|
@@ -27,9 +27,9 @@ export function assertReleaseTagForServerUrl(serverUrl, requested) {
|
|
|
27
27
|
const known = KNOWN_TARGETS[parsed.hostname.toLowerCase()];
|
|
28
28
|
if (known === undefined)
|
|
29
29
|
return;
|
|
30
|
-
|
|
31
|
-
if (requested !==
|
|
32
|
-
throw new Error(`Server hostname '${parsed.hostname}' requires release tag '
|
|
30
|
+
// dev is a compatibility lane: beta remains the default, but stable is also valid.
|
|
31
|
+
if (known.environment !== "dev" && requested !== "latest") {
|
|
32
|
+
throw new Error(`Server hostname '${parsed.hostname}' requires release tag 'latest'`);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
function parseServerUrl(value) {
|
|
@@ -88,7 +88,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
88
88
|
daemonHome: local.daemonHome,
|
|
89
89
|
agentsRoot: paths.join(local.root, environment === "production" ? "agents" : "agents-dev"),
|
|
90
90
|
npmPrefix: local.runtimePrefix,
|
|
91
|
-
releaseTag: environment === "dev" ? "beta" : "latest",
|
|
91
|
+
releaseTag: input.requestedReleaseTag ?? (environment === "dev" ? "beta" : "latest"),
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
if (requestedProfile === "in" || requestedProfile === "dev") {
|
|
@@ -125,7 +125,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
125
125
|
daemonHome: local.daemonHome,
|
|
126
126
|
agentsRoot: paths.join(local.root, "agents-" + layout.environment + "-" + layout.localInstanceName),
|
|
127
127
|
npmPrefix: local.runtimePrefix,
|
|
128
|
-
releaseTag: environment === "dev" ? "beta" : "latest",
|
|
128
|
+
releaseTag: input.requestedReleaseTag ?? (environment === "dev" ? "beta" : "latest"),
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
export function resolveAutomaticInstallTarget(input) {
|
|
@@ -133,9 +133,8 @@ export function resolveAutomaticInstallTarget(input) {
|
|
|
133
133
|
const hostname = parsed.hostname.toLowerCase();
|
|
134
134
|
const paths = input.platform === "win32" ? win32 : posix;
|
|
135
135
|
const known = KNOWN_TARGETS[hostname];
|
|
136
|
-
const targetReleaseTag =
|
|
137
|
-
? "beta"
|
|
138
|
-
: input.requestedReleaseTag ?? "latest";
|
|
136
|
+
const targetReleaseTag = input.requestedReleaseTag
|
|
137
|
+
?? (known?.environment === "dev" ? "beta" : "latest");
|
|
139
138
|
const registryUrl = input.requestedRegistryUrl === undefined
|
|
140
139
|
? undefined
|
|
141
140
|
: normalizeDaemonNpmRegistry(input.requestedRegistryUrl, { allowLoopbackHttp: targetReleaseTag === "beta" });
|
package/dist/execution-runner.js
CHANGED
|
@@ -25,6 +25,7 @@ import { PROJECT_WORKSPACES_CAPABILITY } from "./machine-info.js";
|
|
|
25
25
|
import { PROJECT_SKILL_PROJECTION_V2_CAPABILITY } from "./project-skills/types.js";
|
|
26
26
|
import { dslog } from "./slog.js";
|
|
27
27
|
import { identityRosterPromptFacts } from "./identity-roster-telemetry.js";
|
|
28
|
+
import { renderLocalMemoryV2WriteGuard } from "./local-memory/runtime.js";
|
|
28
29
|
export { supervisorLaunch } from "./supervised-runtime.js";
|
|
29
30
|
const MAX_LOCAL_EXECUTION_ATTEMPTS = 3;
|
|
30
31
|
const ACTIVITY_KIND = {
|
|
@@ -828,7 +829,8 @@ export async function runExecution(config, input, dependencies) {
|
|
|
828
829
|
const baseSystemPromptBudget = spec.agent.abilityRelease === undefined
|
|
829
830
|
? systemPromptBudget
|
|
830
831
|
: Math.max(0, Math.min(systemPromptBudget, Math.max(32 * 1024, systemPromptBudget - 168 * 1024)));
|
|
831
|
-
const
|
|
832
|
+
const localMemoryV2Enabled = config.localMemoryV2Handles?.has(spec.agent.handle) === true;
|
|
833
|
+
const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${localMemoryV2Enabled ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
|
|
832
834
|
...(selfTraining ? { isolateLocalMemory: true } : {}),
|
|
833
835
|
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
834
836
|
});
|
|
@@ -887,7 +889,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
887
889
|
maxTurns: config.sessionMaxTurns,
|
|
888
890
|
},
|
|
889
891
|
...(selfTraining ? { maintenancePurpose: "self_training" } : {}),
|
|
890
|
-
localMemoryV2Enabled
|
|
892
|
+
localMemoryV2Enabled,
|
|
891
893
|
};
|
|
892
894
|
const result = await Promise.race([
|
|
893
895
|
execute(localInput, callbacks, localDependencies),
|
package/dist/local-executor.js
CHANGED
|
@@ -37,6 +37,7 @@ import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
|
37
37
|
import { machinePackageManagerEnv, packageManagerEnvFacts } from "./package-manager-env.js";
|
|
38
38
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
39
39
|
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, renderLocalMemoryResultInstructions, } from "./local-memory/runtime.js";
|
|
40
|
+
import { renderPendingMemoryContext, searchPendingMemoryCandidates, } from "./local-memory/pending.js";
|
|
40
41
|
export class RuntimeRunningCallbackTimeoutError extends Error {
|
|
41
42
|
constructor(runtime, timeoutMs) {
|
|
42
43
|
super(`${runtime} runtime-running callback timed out after ${timeoutMs}ms`);
|
|
@@ -381,6 +382,26 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
381
382
|
}
|
|
382
383
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
383
384
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
385
|
+
let pendingMemoryContext = "";
|
|
386
|
+
if (input.localMemoryV2Enabled === true) {
|
|
387
|
+
try {
|
|
388
|
+
const pendingCandidates = await searchPendingMemoryCandidates({
|
|
389
|
+
agentDir: executionWorkspace.dir,
|
|
390
|
+
query: wakePrompt,
|
|
391
|
+
limit: 5,
|
|
392
|
+
});
|
|
393
|
+
pendingMemoryContext = renderPendingMemoryContext(pendingCandidates);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
dslog("local_memory.pending_recall_failed", "本地归档记忆召回失败", {
|
|
397
|
+
level: "WARN",
|
|
398
|
+
execution_id: input.executionId,
|
|
399
|
+
agent_handle: input.handle,
|
|
400
|
+
error_name: error instanceof Error ? error.name : "unknown",
|
|
401
|
+
error_code: error?.code,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
384
405
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
385
406
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
386
407
|
&& isLocalMemoryV2Eligible({
|
|
@@ -392,14 +413,21 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
392
413
|
})) {
|
|
393
414
|
localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
|
|
394
415
|
}
|
|
395
|
-
const
|
|
416
|
+
const promptWithoutPending = resolvePrompt(input.systemPrompt, promptContext);
|
|
417
|
+
let baseSystemPrompt = `${promptWithoutPending}${pendingMemoryContext}`;
|
|
396
418
|
const structuredMemoryInstructions = localMemoryResultPath === null
|
|
397
419
|
? null
|
|
398
420
|
: renderLocalMemoryResultInstructions(localMemoryResultPath);
|
|
399
|
-
const
|
|
421
|
+
const fitsPromptBudget = (prompt) => structuredMemoryInstructions === null
|
|
400
422
|
|| input.maxSystemPromptBytes === undefined
|
|
401
|
-
|| Buffer.byteLength(
|
|
423
|
+
|| Buffer.byteLength(prompt, "utf8") + Buffer.byteLength(structuredMemoryInstructions, "utf8")
|
|
402
424
|
<= input.maxSystemPromptBytes;
|
|
425
|
+
// The structured prune contract is mandatory for V2. Drop optional archived
|
|
426
|
+
// recall before dropping the daemon result file when the prompt budget is tight.
|
|
427
|
+
if (!fitsPromptBudget(baseSystemPrompt) && pendingMemoryContext !== "") {
|
|
428
|
+
baseSystemPrompt = promptWithoutPending;
|
|
429
|
+
}
|
|
430
|
+
const structuredPromptFits = fitsPromptBudget(baseSystemPrompt);
|
|
403
431
|
if (!structuredPromptFits)
|
|
404
432
|
localMemoryResultPath = null;
|
|
405
433
|
const systemPrompt = localMemoryResultPath === null
|
|
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { lstat, mkdir, readdir, realpath, rename, rm, writeFile, readFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
export const LOCAL_MEMORY_PENDING_SCAN_FILE_CAP = 128;
|
|
6
|
+
export const LOCAL_MEMORY_PENDING_SCAN_BYTE_CAP = 1_000_000;
|
|
5
7
|
const CandidateSchema = z.object({
|
|
6
8
|
operation: z.enum(["create", "update", "supersede"]),
|
|
7
9
|
memoryId: z.string().nullable(),
|
|
@@ -59,6 +61,44 @@ async function pendingDirectory(agentDir) {
|
|
|
59
61
|
}
|
|
60
62
|
return directory;
|
|
61
63
|
}
|
|
64
|
+
async function existingPendingDirectory(agentDir) {
|
|
65
|
+
const agentRoot = await realpath(agentDir);
|
|
66
|
+
const memoryRoot = resolve(agentRoot, ".crew", "memory");
|
|
67
|
+
let memoryStat;
|
|
68
|
+
try {
|
|
69
|
+
memoryStat = await lstat(memoryRoot);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code === "ENOENT")
|
|
73
|
+
return null;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink()) {
|
|
77
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
78
|
+
}
|
|
79
|
+
const resolvedMemoryRoot = await realpath(memoryRoot);
|
|
80
|
+
if (resolvedMemoryRoot !== memoryRoot || !within(agentRoot, resolvedMemoryRoot)) {
|
|
81
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
82
|
+
}
|
|
83
|
+
const directory = join(memoryRoot, "pending");
|
|
84
|
+
let stat;
|
|
85
|
+
try {
|
|
86
|
+
stat = await lstat(directory);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error.code === "ENOENT")
|
|
90
|
+
return null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
94
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
95
|
+
}
|
|
96
|
+
const resolvedDirectory = await realpath(directory);
|
|
97
|
+
if (resolvedDirectory !== directory || !within(agentRoot, resolvedDirectory)) {
|
|
98
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
99
|
+
}
|
|
100
|
+
return directory;
|
|
101
|
+
}
|
|
62
102
|
async function assertRegularFile(path) {
|
|
63
103
|
try {
|
|
64
104
|
const stat = await lstat(path);
|
|
@@ -91,15 +131,40 @@ export async function persistPendingMemoryCandidate(input) {
|
|
|
91
131
|
await atomicWrite(target, `${JSON.stringify(parsed.data, null, 2)}\n`);
|
|
92
132
|
return target;
|
|
93
133
|
}
|
|
134
|
+
export async function removePendingMemoryCandidate(input) {
|
|
135
|
+
const directory = await existingPendingDirectory(input.agentDir);
|
|
136
|
+
if (directory === null)
|
|
137
|
+
return;
|
|
138
|
+
const target = join(directory, `${input.pendingId}.json`);
|
|
139
|
+
await assertRegularFile(target);
|
|
140
|
+
await rm(target, { force: true });
|
|
141
|
+
}
|
|
94
142
|
export async function readPendingMemoryCandidates(input) {
|
|
95
|
-
const directory = await
|
|
143
|
+
const directory = await existingPendingDirectory(input.agentDir);
|
|
144
|
+
if (directory === null)
|
|
145
|
+
return [];
|
|
96
146
|
const names = (await readdir(directory)).filter((name) => name.endsWith(".json")).sort();
|
|
147
|
+
const maxFiles = Math.max(0, input.maxFiles ?? LOCAL_MEMORY_PENDING_SCAN_FILE_CAP);
|
|
148
|
+
const maxBytes = Math.max(0, input.maxBytes ?? LOCAL_MEMORY_PENDING_SCAN_BYTE_CAP);
|
|
97
149
|
const result = [];
|
|
150
|
+
let scannedBytes = 0;
|
|
98
151
|
for (const name of names) {
|
|
152
|
+
if (result.length >= maxFiles || scannedBytes >= maxBytes)
|
|
153
|
+
break;
|
|
99
154
|
const path = join(directory, name);
|
|
100
155
|
try {
|
|
101
156
|
await assertRegularFile(path);
|
|
102
|
-
const
|
|
157
|
+
const stat = await lstat(path);
|
|
158
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
159
|
+
throw new Error("local_memory_pending_path_invalid");
|
|
160
|
+
if (scannedBytes + stat.size > maxBytes)
|
|
161
|
+
break;
|
|
162
|
+
const raw = await readFile(path, "utf8");
|
|
163
|
+
const size = Buffer.byteLength(raw, "utf8");
|
|
164
|
+
if (scannedBytes + size > maxBytes)
|
|
165
|
+
break;
|
|
166
|
+
scannedBytes += size;
|
|
167
|
+
const parsed = PendingSchema.safeParse(JSON.parse(raw));
|
|
103
168
|
if (parsed.success && `${parsed.data.pendingId}.json` === name)
|
|
104
169
|
result.push(parsed.data);
|
|
105
170
|
}
|
|
@@ -109,3 +174,72 @@ export async function readPendingMemoryCandidates(input) {
|
|
|
109
174
|
}
|
|
110
175
|
return result;
|
|
111
176
|
}
|
|
177
|
+
function normalize(value) {
|
|
178
|
+
return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
|
|
179
|
+
}
|
|
180
|
+
function priorityRank(priority) {
|
|
181
|
+
return priority === "high" ? 0 : priority === "medium" ? 1 : 2;
|
|
182
|
+
}
|
|
183
|
+
function candidateText(candidate) {
|
|
184
|
+
return [
|
|
185
|
+
candidate.candidate.title,
|
|
186
|
+
candidate.candidate.summary,
|
|
187
|
+
...candidate.candidate.triggers,
|
|
188
|
+
candidate.candidate.detailCategory,
|
|
189
|
+
candidate.candidate.detailBody,
|
|
190
|
+
].join("\n");
|
|
191
|
+
}
|
|
192
|
+
function recallScore(candidate, query) {
|
|
193
|
+
const normalizedQuery = normalize(query);
|
|
194
|
+
if (normalizedQuery === "")
|
|
195
|
+
return 1;
|
|
196
|
+
const text = normalize(candidateText(candidate));
|
|
197
|
+
if (text.includes(normalizedQuery))
|
|
198
|
+
return 100;
|
|
199
|
+
const terms = normalizedQuery.split(/[\s,,、。!?;::()[\]{}"'`]+/u).filter((term) => term.length > 0);
|
|
200
|
+
return terms.reduce((score, term) => score + (text.includes(term) ? 1 : 0), 0);
|
|
201
|
+
}
|
|
202
|
+
export async function searchPendingMemoryCandidates(input) {
|
|
203
|
+
const candidates = await readPendingMemoryCandidates({ agentDir: input.agentDir });
|
|
204
|
+
const limit = Math.max(0, input.limit ?? 5);
|
|
205
|
+
if (limit === 0)
|
|
206
|
+
return [];
|
|
207
|
+
return candidates
|
|
208
|
+
.map((candidate) => ({ candidate, score: recallScore(candidate, input.query) }))
|
|
209
|
+
.filter((item) => normalize(input.query) === "" || item.score > 0)
|
|
210
|
+
.sort((left, right) => right.score - left.score
|
|
211
|
+
|| priorityRank(left.candidate.candidate.priority) - priorityRank(right.candidate.candidate.priority)
|
|
212
|
+
|| right.candidate.createdAt.localeCompare(left.candidate.createdAt)
|
|
213
|
+
|| left.candidate.pendingId.localeCompare(right.candidate.pendingId))
|
|
214
|
+
.slice(0, limit)
|
|
215
|
+
.map((item) => item.candidate);
|
|
216
|
+
}
|
|
217
|
+
function truncateCodePoints(value, maxCodePoints) {
|
|
218
|
+
return Array.from(value).slice(0, Math.max(0, maxCodePoints)).join("");
|
|
219
|
+
}
|
|
220
|
+
export function renderPendingMemoryContext(candidates, maxCodePoints = 2_000) {
|
|
221
|
+
if (candidates.length === 0 || maxCodePoints <= 0)
|
|
222
|
+
return "";
|
|
223
|
+
const header = `\n\n## Archived memory candidates (daemon-managed)\n这些记忆已持久化,但尚未进入常驻热索引;只在当前请求相关时参考,必要时再由 memory-prune 提升。\n`;
|
|
224
|
+
let remaining = maxCodePoints - Array.from(header).length;
|
|
225
|
+
if (remaining <= 0)
|
|
226
|
+
return truncateCodePoints(header, maxCodePoints);
|
|
227
|
+
const sections = [];
|
|
228
|
+
for (const item of candidates) {
|
|
229
|
+
const change = item.candidate;
|
|
230
|
+
const prefix = `\n### ${change.title}\n`
|
|
231
|
+
+ `- pending_file: .crew/memory/pending/${item.pendingId}.json\n`
|
|
232
|
+
+ `- summary: ${change.summary}\n`
|
|
233
|
+
+ `- triggers: ${change.triggers.join(", ")}\n`
|
|
234
|
+
+ "- detail: ";
|
|
235
|
+
const prefixLength = Array.from(prefix).length;
|
|
236
|
+
if (prefixLength + 1 > remaining)
|
|
237
|
+
break;
|
|
238
|
+
const detail = truncateCodePoints(change.detailBody, remaining - prefixLength - 1);
|
|
239
|
+
const section = `${prefix}${detail}\n`;
|
|
240
|
+
const length = Array.from(section).length;
|
|
241
|
+
sections.push(section);
|
|
242
|
+
remaining -= length;
|
|
243
|
+
}
|
|
244
|
+
return `${header}${sections.join("")}`;
|
|
245
|
+
}
|
|
@@ -57,9 +57,16 @@ ${resultPath}
|
|
|
57
57
|
JSON 顶层必须包含:
|
|
58
58
|
{"schemaVersion":1,"purpose":"memory_prune","baseCatalogRevision":0,"outcome":"no_change","changes":[],"statusChanges":[]}
|
|
59
59
|
每个 changes 项使用 operation=create|update|supersede、title、kind、summary、triggers、priority、detailCategory、detailBody、reviewAfter;create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null,update/supersede 按现有索引填写版本和 id。
|
|
60
|
-
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending
|
|
60
|
+
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中;后续相关任务会按需召回,索引腾出空间后可自动提升。
|
|
61
61
|
`;
|
|
62
62
|
}
|
|
63
|
+
/** Prevent ordinary V2 turns from racing the daemon-managed memory writer. */
|
|
64
|
+
export function renderLocalMemoryV2WriteGuard() {
|
|
65
|
+
return `
|
|
66
|
+
|
|
67
|
+
## V2 shared memory write safety
|
|
68
|
+
本节覆盖前文关于普通任务直接更新共享记忆的例外规则。本 Agent 已启用结构化本地记忆 V2。普通任务不得直接编辑共享的 $CREW_HOME/MEMORY.md 或 $CREW_HOME/notes/ 文件;即使用户明确要求记住内容,也只把候选、用户授权和分类理由记录到本任务的 $CREW_TASK_LOG,留给任务结束后的 memory-prune 结构化收尾应用。只有 memory-prune 收尾提示明确要求写入 daemon 提供的 result 文件时,才按该协议提交候选。`;
|
|
69
|
+
}
|
|
63
70
|
export async function commitLocalMemoryResultAfterExit(input) {
|
|
64
71
|
if (input.exitCode !== 0)
|
|
65
72
|
return { status: "skipped" };
|
|
@@ -4,7 +4,7 @@ import { dirname, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { containsLikelySecret } from "../agent-memory/policy.js";
|
|
6
6
|
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderLocalMemoryIndexCard, selectLocalMemoryIndexEntries, } from "./index-budget.js";
|
|
7
|
-
import { persistPendingMemoryCandidate } from "./pending.js";
|
|
7
|
+
import { persistPendingMemoryCandidate, readPendingMemoryCandidates, removePendingMemoryCandidate, } from "./pending.js";
|
|
8
8
|
const CategorySchema = z.enum([
|
|
9
9
|
"preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
|
|
10
10
|
]);
|
|
@@ -228,6 +228,47 @@ function normalize(value) {
|
|
|
228
228
|
function activeOrPinnedIds(entries) {
|
|
229
229
|
return new Set(entries.filter((entry) => entry.status === "active" || entry.status === "pinned").map((entry) => entry.id));
|
|
230
230
|
}
|
|
231
|
+
function pinnedIds(entries) {
|
|
232
|
+
return new Set(entries.filter((entry) => entry.status === "pinned").map((entry) => entry.id));
|
|
233
|
+
}
|
|
234
|
+
const DETAIL_CATEGORIES = new Set([
|
|
235
|
+
"preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
|
|
236
|
+
]);
|
|
237
|
+
function detailCategoryFromFile(file) {
|
|
238
|
+
if (!file.startsWith("notes/") || !file.endsWith(".md"))
|
|
239
|
+
return null;
|
|
240
|
+
const category = file.slice("notes/".length, -".md".length);
|
|
241
|
+
return DETAIL_CATEGORIES.has(category) ? category : null;
|
|
242
|
+
}
|
|
243
|
+
function readNoteBody(content, id) {
|
|
244
|
+
const begin = content.indexOf(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id}`);
|
|
245
|
+
if (begin < 0)
|
|
246
|
+
return null;
|
|
247
|
+
const bodyStart = content.indexOf("\n\n", begin);
|
|
248
|
+
const bodyEnd = content.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
|
|
249
|
+
if (bodyStart < 0 || bodyEnd < 0)
|
|
250
|
+
return null;
|
|
251
|
+
return content.slice(bodyStart + 2, bodyEnd).trim();
|
|
252
|
+
}
|
|
253
|
+
function archivedChange(entry, detailBody) {
|
|
254
|
+
const detailCategory = detailCategoryFromFile(entry.file);
|
|
255
|
+
if (detailCategory === null)
|
|
256
|
+
return null;
|
|
257
|
+
return {
|
|
258
|
+
operation: "create",
|
|
259
|
+
memoryId: null,
|
|
260
|
+
baseRevision: null,
|
|
261
|
+
supersedesMemoryId: null,
|
|
262
|
+
title: entry.title,
|
|
263
|
+
kind: entry.type,
|
|
264
|
+
summary: entry.summary,
|
|
265
|
+
triggers: entry.triggers,
|
|
266
|
+
priority: entry.priority,
|
|
267
|
+
detailCategory,
|
|
268
|
+
detailBody,
|
|
269
|
+
reviewAfter: entry.reviewAfter,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
231
272
|
function emptyResult() {
|
|
232
273
|
return { createdIds: [], updatedIds: [], statusChangedIds: [], deferredIds: [], pendingIds: [] };
|
|
233
274
|
}
|
|
@@ -341,7 +382,9 @@ export async function applyLocalMemoryResult(input) {
|
|
|
341
382
|
const statusChangedIds = input.result.statusChanges.map((change) => change.memoryId);
|
|
342
383
|
const deferredIds = [];
|
|
343
384
|
const pendingCandidates = [];
|
|
385
|
+
const resolvedPendingIds = new Set();
|
|
344
386
|
const noteUpdates = new Map();
|
|
387
|
+
let allowActiveEviction = false;
|
|
345
388
|
const randomId = input.randomId ?? randomUUID;
|
|
346
389
|
const executionId = input.executionId ?? "unknown";
|
|
347
390
|
const defer = (change, index) => {
|
|
@@ -399,7 +442,10 @@ export async function applyLocalMemoryResult(input) {
|
|
|
399
442
|
: item),
|
|
400
443
|
entry,
|
|
401
444
|
];
|
|
402
|
-
const
|
|
445
|
+
const allowEvictionForChange = change.operation === "create" && change.priority === "high";
|
|
446
|
+
const protectedIds = new Set(allowEvictionForChange ? pinnedIds(entries) : activeOrPinnedIds(entries));
|
|
447
|
+
if (allowEvictionForChange)
|
|
448
|
+
allowActiveEviction = true;
|
|
403
449
|
if (change.supersedesMemoryId !== null)
|
|
404
450
|
protectedIds.delete(change.supersedesMemoryId);
|
|
405
451
|
const projection = projectIndex(memory, candidateEntries, protectedIds);
|
|
@@ -428,15 +474,91 @@ export async function applyLocalMemoryResult(input) {
|
|
|
428
474
|
else if (existingCreate === undefined)
|
|
429
475
|
createdIds.push(id);
|
|
430
476
|
}
|
|
431
|
-
|
|
432
|
-
|
|
477
|
+
// Pending create candidates are durable overflow, not dead ends. Promote only
|
|
478
|
+
// when the current catalog has room; supersede/update candidates stay pending
|
|
479
|
+
// until a fresh revision-aware decision can safely replay them.
|
|
480
|
+
const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir });
|
|
481
|
+
for (const pending of pendingCandidatesOnDisk) {
|
|
482
|
+
const change = pending.candidate;
|
|
483
|
+
if (change.operation !== "create")
|
|
484
|
+
continue;
|
|
485
|
+
const id = stableId(`pending:${pending.pendingId}`, 0, change);
|
|
486
|
+
const existing = entries.find((entry) => entry.id === id);
|
|
487
|
+
if (existing !== undefined) {
|
|
488
|
+
resolvedPendingIds.add(pending.pendingId);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const duplicate = entries.find((entry) => (entry.status === "active" || entry.status === "pinned")
|
|
492
|
+
&& (normalize(entry.title) === normalize(change.title)
|
|
493
|
+
|| normalize(entry.summary) === normalize(change.summary)));
|
|
494
|
+
if (duplicate !== undefined) {
|
|
495
|
+
const duplicatePath = resolve(input.agentDir, duplicate.file);
|
|
496
|
+
const duplicateBody = readNoteBody(noteUpdates.get(duplicatePath) ?? await readNote(duplicatePath), duplicate.id);
|
|
497
|
+
if (duplicateBody === change.detailBody)
|
|
498
|
+
resolvedPendingIds.add(pending.pendingId);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const entry = entryFromChange(change, id, 1, "active");
|
|
502
|
+
const candidateEntries = [...entries, entry];
|
|
503
|
+
const allowEvictionForPending = change.priority === "high";
|
|
504
|
+
const projection = projectIndex(memory, candidateEntries, allowEvictionForPending ? pinnedIds(entries) : activeOrPinnedIds(entries));
|
|
505
|
+
if (allowEvictionForPending)
|
|
506
|
+
allowActiveEviction = true;
|
|
507
|
+
if (projection.omittedIds.includes(id))
|
|
508
|
+
continue;
|
|
509
|
+
const notePath = resolve(input.agentDir, entry.file);
|
|
510
|
+
if (!isWithin(resolve(input.agentDir, "notes"), notePath))
|
|
511
|
+
throw new Error("local_memory_note_path_invalid");
|
|
512
|
+
const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
|
|
513
|
+
const replacement = noteSection(id, 1, change.title, change.detailBody);
|
|
514
|
+
const existingSection = replaceNote(previous, id, replacement);
|
|
515
|
+
const next = existingSection ?? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`;
|
|
516
|
+
noteUpdates.set(notePath, next);
|
|
517
|
+
entries = candidateEntries;
|
|
518
|
+
createdIds.push(id);
|
|
519
|
+
resolvedPendingIds.add(pending.pendingId);
|
|
520
|
+
}
|
|
521
|
+
let finalProjection = projectIndex(memory, entries, allowActiveEviction ? pinnedIds(entries) : activeOrPinnedIds(entries));
|
|
522
|
+
if (finalProjection.reason !== null
|
|
523
|
+
&& (!allowActiveEviction || finalProjection.reason === "index_over_budget_pinned")) {
|
|
433
524
|
throw new Error(finalProjection.reason);
|
|
525
|
+
}
|
|
526
|
+
if (allowActiveEviction) {
|
|
527
|
+
const omittedActiveEntries = finalProjection.omittedIds
|
|
528
|
+
.map((id) => entries.find((entry) => entry.id === id))
|
|
529
|
+
.filter((entry) => entry?.status === "active");
|
|
530
|
+
for (const entry of omittedActiveEntries) {
|
|
531
|
+
const notePath = resolve(input.agentDir, entry.file);
|
|
532
|
+
const content = noteUpdates.get(notePath) ?? await readNote(notePath);
|
|
533
|
+
const detailBody = readNoteBody(content, entry.id);
|
|
534
|
+
const change = detailBody === null ? null : archivedChange(entry, detailBody);
|
|
535
|
+
if (change === null)
|
|
536
|
+
throw new Error("local_memory_note_anchor_missing");
|
|
537
|
+
const pendingId = `pending-${stableId(`evicted:${executionId}`, 0, change).slice("mem_".length)}`;
|
|
538
|
+
pendingCandidates.push({
|
|
539
|
+
schemaVersion: 1,
|
|
540
|
+
pendingId,
|
|
541
|
+
createdAt: new Date().toISOString(),
|
|
542
|
+
reason: "index_over_budget",
|
|
543
|
+
source: { executionId, pruneTraceId: input.pruneTraceId ?? null },
|
|
544
|
+
candidate: change,
|
|
545
|
+
});
|
|
546
|
+
deferredIds.push(pendingId);
|
|
547
|
+
entries = entries.map((item) => item.id === entry.id ? { ...item, status: "archived" } : item);
|
|
548
|
+
}
|
|
549
|
+
finalProjection = projectIndex(memory, entries, pinnedIds(entries));
|
|
550
|
+
if (finalProjection.reason !== null)
|
|
551
|
+
throw new Error(finalProjection.reason);
|
|
552
|
+
}
|
|
434
553
|
for (const candidate of pendingCandidates)
|
|
435
554
|
await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
|
|
436
555
|
for (const [path, content] of noteUpdates)
|
|
437
556
|
await atomicWrite(path, content);
|
|
438
557
|
if (noteUpdates.size > 0 || statusChangedIds.length > 0)
|
|
439
558
|
await atomicWrite(memoryPath, finalProjection.memory);
|
|
559
|
+
for (const pendingId of resolvedPendingIds) {
|
|
560
|
+
await removePendingMemoryCandidate({ agentDir: input.agentDir, pendingId });
|
|
561
|
+
}
|
|
440
562
|
return {
|
|
441
563
|
createdIds,
|
|
442
564
|
updatedIds,
|
package/dist/main.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { PROJECT_SKILLS_CAPABILITY, PROJECT_SKILL_PROJECTION_V2_CAPABILITY, } from "./types.js";
|
|
2
|
+
export function projectSkillV2RolloutReady(platform, readiness) {
|
|
3
|
+
const platformReady = platform === "darwin"
|
|
4
|
+
|| platform === "linux"
|
|
5
|
+
|| (platform === "win32" && readiness.nativeWindowsSmokePassed);
|
|
6
|
+
return platformReady
|
|
7
|
+
&& readiness.runtimeRootStorePrimitiveReady
|
|
8
|
+
&& readiness.runtimeRootStoreRecoveryReady
|
|
9
|
+
&& readiness.leaseRecoveryReady
|
|
10
|
+
&& readiness.controllerReady
|
|
11
|
+
&& readiness.executionHandlerReady
|
|
12
|
+
&& readiness.codexAdapterReady
|
|
13
|
+
&& readiness.claudeAdapterReady;
|
|
14
|
+
}
|
|
15
|
+
export function effectiveProjectSkillCapabilities(capabilities, projectSkillsReady, projectSkillV2Ready) {
|
|
16
|
+
const projectSkillCapabilitiesReady = projectSkillsReady && projectSkillV2Ready;
|
|
17
|
+
if (!projectSkillCapabilitiesReady
|
|
18
|
+
&& !capabilities.includes(PROJECT_SKILLS_CAPABILITY)
|
|
19
|
+
&& !capabilities.includes(PROJECT_SKILL_PROJECTION_V2_CAPABILITY)
|
|
20
|
+
&& Object.isFrozen(capabilities))
|
|
21
|
+
return capabilities;
|
|
22
|
+
const filtered = capabilities.filter((capability) => capability !== PROJECT_SKILL_PROJECTION_V2_CAPABILITY
|
|
23
|
+
&& (projectSkillCapabilitiesReady || capability !== PROJECT_SKILLS_CAPABILITY));
|
|
24
|
+
if (!projectSkillCapabilitiesReady || !filtered.includes(PROJECT_SKILLS_CAPABILITY)) {
|
|
25
|
+
return Object.freeze(filtered);
|
|
26
|
+
}
|
|
27
|
+
const v1Index = filtered.indexOf(PROJECT_SKILLS_CAPABILITY);
|
|
28
|
+
return Object.freeze([
|
|
29
|
+
...filtered.slice(0, v1Index + 1),
|
|
30
|
+
PROJECT_SKILL_PROJECTION_V2_CAPABILITY,
|
|
31
|
+
...filtered.slice(v1Index + 1),
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
export function withProjectSkillCapabilityStatus(value, status) {
|
|
35
|
+
return {
|
|
36
|
+
...value,
|
|
37
|
+
projectSkillsStatus: status,
|
|
38
|
+
capabilities: status === "ready"
|
|
39
|
+
? value.capabilities
|
|
40
|
+
: effectiveProjectSkillCapabilities(value.capabilities, false, false),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const settleReadinessGate = async (operation) => {
|
|
44
|
+
try {
|
|
45
|
+
return await operation() === true;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export async function createProjectSkillConnectionCapabilitySnapshot(input) {
|
|
52
|
+
const runtimeRootStorePrimitiveReady = await settleReadinessGate(input.probeRuntimeRootStorePrimitive);
|
|
53
|
+
const controllerReady = await settleReadinessGate(input.initializeController);
|
|
54
|
+
const recoveryReady = await settleReadinessGate(input.recoverRuntimeRootsAndLeases);
|
|
55
|
+
const readiness = Object.freeze({
|
|
56
|
+
runtimeRootStorePrimitiveReady,
|
|
57
|
+
runtimeRootStoreRecoveryReady: recoveryReady,
|
|
58
|
+
leaseRecoveryReady: recoveryReady,
|
|
59
|
+
controllerReady,
|
|
60
|
+
executionHandlerReady: true,
|
|
61
|
+
codexAdapterReady: true,
|
|
62
|
+
claudeAdapterReady: true,
|
|
63
|
+
nativeWindowsSmokePassed: false,
|
|
64
|
+
});
|
|
65
|
+
const projectSkillsReady = projectSkillV2RolloutReady(input.platform, readiness);
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
capabilities: effectiveProjectSkillCapabilities(input.baseCapabilities, projectSkillsReady, projectSkillsReady),
|
|
68
|
+
projectSkillsReady,
|
|
69
|
+
readiness,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export async function prepareProjectSkillConnectionCapabilities(input) {
|
|
73
|
+
const snapshot = await createProjectSkillConnectionCapabilitySnapshot({
|
|
74
|
+
...input,
|
|
75
|
+
baseCapabilities: [...input.baseCapabilities, ...await input.additionalCapabilities()],
|
|
76
|
+
});
|
|
77
|
+
input.applyReadiness(snapshot.projectSkillsReady);
|
|
78
|
+
return snapshot.capabilities;
|
|
79
|
+
}
|
|
80
|
+
export function createProjectSkillControllerReadiness(input) {
|
|
81
|
+
let status = "initializing";
|
|
82
|
+
let initialization = null;
|
|
83
|
+
const ensure = () => {
|
|
84
|
+
if (status === "ready")
|
|
85
|
+
return Promise.resolve(true);
|
|
86
|
+
if (initialization !== null)
|
|
87
|
+
return initialization;
|
|
88
|
+
status = "initializing";
|
|
89
|
+
const attempt = input.initialize().then(() => {
|
|
90
|
+
status = "ready";
|
|
91
|
+
return true;
|
|
92
|
+
}, (error) => {
|
|
93
|
+
status = "unavailable";
|
|
94
|
+
input.onUnavailable(error);
|
|
95
|
+
return false;
|
|
96
|
+
}).finally(() => {
|
|
97
|
+
if (initialization === attempt)
|
|
98
|
+
initialization = null;
|
|
99
|
+
input.onSettled();
|
|
100
|
+
});
|
|
101
|
+
initialization = attempt;
|
|
102
|
+
return attempt;
|
|
103
|
+
};
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
status: () => status,
|
|
106
|
+
ensure,
|
|
107
|
+
applyPreflight: (ready) => { status = ready ? "ready" : "unavailable"; },
|
|
108
|
+
});
|
|
109
|
+
}
|