@nowcrew/daemon 0.6.58 → 0.6.63
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/dist/agent-ability/materializer.js +1 -20
- package/dist/automatic-install-target.js +10 -15
- package/dist/config.js +0 -35
- package/dist/daemon-migration-controller.js +18 -16
- package/dist/execution-runner.js +0 -1
- package/dist/local-executor.js +1 -44
- package/dist/machine-info.js +2 -3
- package/dist/main.js +0 -77
- package/dist/profile-layout.js +4 -10
- package/dist/runner.js +0 -1
- package/dist/serve.js +3 -64
- package/dist/workspace.js +1 -24
- package/package.json +10 -9
- package/dist/local-memory/index-budget.js +0 -74
- package/dist/local-memory/pending.js +0 -111
- package/dist/local-memory/runtime.js +0 -87
- package/dist/local-memory/types.js +0 -1
- package/dist/local-memory/write.js +0 -448
- package/dist/storage-config.js +0 -175
- package/dist/storage-control.js +0 -131
- package/dist/storage-copy.js +0 -444
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -175
- package/dist/storage-identity.js +0 -29
- package/dist/storage-layout.js +0 -143
- package/dist/storage-manifest.js +0 -440
- package/dist/storage-migration-controller.js +0 -417
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -311
- package/dist/storage-runtime-readiness.js +0 -178
- package/dist/storage-runtime.js +0 -39
- package/dist/storage-safe-fs.js +0 -293
- package/dist/storage-serve-adapter.js +0 -65
- package/dist/storage-status-delivery.js +0 -201
- package/dist/storage-status-replay.js +0 -125
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -27
- package/dist/storage-worker-executor.js +0 -380
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -277
- package/dist/storage-worker-status.js +0 -105
|
@@ -249,23 +249,6 @@ const verifyCache = async (agentsRoot, ability) => {
|
|
|
249
249
|
};
|
|
250
250
|
const layerRelativePath = (path, layer) => path.startsWith(`${layer}/`) ? path.slice(layer.length + 1) : path;
|
|
251
251
|
const readManagedCatalog = async (trainingRoot) => JSON.parse(await readFile(join(trainingRoot, "ACTIVE_RELEASE.json"), "utf8"));
|
|
252
|
-
const projectedSkillExists = async (trainingRoot, name) => {
|
|
253
|
-
const root = join(trainingRoot, "skills", safeAbilityName(name));
|
|
254
|
-
if (!await isRealDirectory(root))
|
|
255
|
-
return false;
|
|
256
|
-
const direct = await lstat(join(root, "SKILL.md")).catch(() => null);
|
|
257
|
-
if (direct?.isFile() && !direct.isSymbolicLink())
|
|
258
|
-
return true;
|
|
259
|
-
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
260
|
-
if (!entry.isDirectory())
|
|
261
|
-
continue;
|
|
262
|
-
const skill = await lstat(join(root, entry.name, "SKILL.md")).catch(() => null);
|
|
263
|
-
if (skill?.isFile() && !skill.isSymbolicLink())
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
return false;
|
|
267
|
-
};
|
|
268
|
-
const projectedSkillsComplete = async (trainingRoot, skills) => (await Promise.all(skills.map((skill) => projectedSkillExists(trainingRoot, skill.name)))).every(Boolean);
|
|
269
252
|
const trainingSwitchJournalPath = (agentRoot) => join(agentRoot, ".training-switch.json");
|
|
270
253
|
const trainingRollbackRoot = (agentRoot) => join(agentRoot, ".training-rollback");
|
|
271
254
|
const trainingDiscardRoot = (agentRoot) => join(agentRoot, ".training-discard");
|
|
@@ -439,7 +422,6 @@ const activeTrainingRoot = async (agentsRoot, handle, ability) => {
|
|
|
439
422
|
&& catalog.releaseId === ability.releaseId
|
|
440
423
|
&& catalog.rootCommit === ability.rootCommit
|
|
441
424
|
&& catalog.artifactDigest === ability.artifactDigest
|
|
442
|
-
&& await projectedSkillsComplete(trainingRoot, ability.skills)
|
|
443
425
|
? trainingRoot
|
|
444
426
|
: null;
|
|
445
427
|
};
|
|
@@ -481,8 +463,7 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
481
463
|
}
|
|
482
464
|
if (previousCatalog.releaseId === ability.releaseId
|
|
483
465
|
&& previousCatalog.artifactDigest === ability.artifactDigest
|
|
484
|
-
&& previousCatalog.projectionDigest === projectionDigest
|
|
485
|
-
&& await projectedSkillsComplete(trainingRoot, ability.skills))
|
|
466
|
+
&& previousCatalog.projectionDigest === projectionDigest)
|
|
486
467
|
return trainingRoot;
|
|
487
468
|
}
|
|
488
469
|
await mkdir(agentRoot, { recursive: true });
|
|
@@ -3,12 +3,12 @@ import { profileLayout, validateLocalInstanceName } from "./profile-layout.js";
|
|
|
3
3
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
4
4
|
const KNOWN_TARGETS = {
|
|
5
5
|
"nowwork-in.nowcoder.com": {
|
|
6
|
-
|
|
6
|
+
profile: "in",
|
|
7
7
|
daemonDirectory: "daemon",
|
|
8
8
|
agentsDirectory: "agents",
|
|
9
9
|
},
|
|
10
10
|
"nowwork-dev.nowcoder.com": {
|
|
11
|
-
|
|
11
|
+
profile: "dev",
|
|
12
12
|
daemonDirectory: "daemon-dev",
|
|
13
13
|
agentsDirectory: "agents-dev",
|
|
14
14
|
},
|
|
@@ -42,37 +42,32 @@ function requireCustomProfile(hostname, value) {
|
|
|
42
42
|
if (!PROFILE_NAME.test(value)) {
|
|
43
43
|
throw new Error("Invalid custom profile; expected a lowercase name of 1-48 letters, digits, '_' or '-'");
|
|
44
44
|
}
|
|
45
|
-
if (value === "in" || value === "dev"
|
|
45
|
+
if (value === "in" || value === "dev") {
|
|
46
46
|
throw new Error(`Profile '${value}' is reserved for a known NowWork hostname`);
|
|
47
47
|
}
|
|
48
48
|
return value;
|
|
49
49
|
}
|
|
50
50
|
function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
51
51
|
const paths = input.platform === "win32" ? win32 : posix;
|
|
52
|
-
|
|
53
|
-
// uses the canonical environment token (production, never in).
|
|
54
|
-
const legacyProfile = environment === "production" ? "in" : environment;
|
|
52
|
+
const legacyProfile = environment;
|
|
55
53
|
if (requestedProfile === undefined || requestedProfile === legacyProfile) {
|
|
56
54
|
return Object.freeze({
|
|
57
55
|
kind: "known",
|
|
58
56
|
profile: legacyProfile,
|
|
59
57
|
serverUrl: normalizedServerUrl(parsed),
|
|
60
|
-
daemonHome: paths.join(input.userHome, ".crew", environment === "
|
|
61
|
-
agentsRoot: paths.join(input.userHome, ".crew", environment === "
|
|
58
|
+
daemonHome: paths.join(input.userHome, ".crew", environment === "in" ? "daemon" : "daemon-dev"),
|
|
59
|
+
agentsRoot: paths.join(input.userHome, ".crew", environment === "in" ? "agents" : "agents-dev"),
|
|
62
60
|
npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", legacyProfile),
|
|
63
61
|
});
|
|
64
62
|
}
|
|
65
63
|
if (requestedProfile === "in" || requestedProfile === "dev") {
|
|
66
|
-
throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${
|
|
64
|
+
throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${legacyProfile}'`);
|
|
67
65
|
}
|
|
68
66
|
const prefix = `nw-${environment}-`;
|
|
69
|
-
|
|
70
|
-
if (!requestedProfile.startsWith(prefix) && !requestedProfile.startsWith(legacyPrefix)) {
|
|
67
|
+
if (!requestedProfile.startsWith(prefix)) {
|
|
71
68
|
throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${prefix}<local-name>'`);
|
|
72
69
|
}
|
|
73
|
-
const localInstanceName = validateLocalInstanceName(requestedProfile.
|
|
74
|
-
? requestedProfile.slice(prefix.length)
|
|
75
|
-
: requestedProfile.slice(legacyPrefix.length));
|
|
70
|
+
const localInstanceName = validateLocalInstanceName(requestedProfile.slice(prefix.length));
|
|
76
71
|
const layout = profileLayout({
|
|
77
72
|
environment,
|
|
78
73
|
localInstanceName,
|
|
@@ -96,7 +91,7 @@ export function resolveAutomaticInstallTarget(input) {
|
|
|
96
91
|
const paths = input.platform === "win32" ? win32 : posix;
|
|
97
92
|
const known = KNOWN_TARGETS[hostname];
|
|
98
93
|
if (known !== undefined) {
|
|
99
|
-
return knownTargetForProfile(known.
|
|
94
|
+
return knownTargetForProfile(known.profile, input.requestedProfile, input, parsed);
|
|
100
95
|
}
|
|
101
96
|
const profile = requireCustomProfile(hostname, input.requestedProfile);
|
|
102
97
|
return Object.freeze({
|
package/dist/config.js
CHANGED
|
@@ -59,40 +59,6 @@ function defaultCliPath() {
|
|
|
59
59
|
return resolve(here, "../../cli/dist/main.js");
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
// 临时 dev 验收名单。测试完成后删除此常量,恢复未配置时默认关闭的行为。
|
|
63
|
-
const TEMP_LOCAL_MEMORY_V2_TEST_HANDLES = [
|
|
64
|
-
"memory-v2-test-a",
|
|
65
|
-
"memory-v2-test-b",
|
|
66
|
-
"memory-v2-test-c",
|
|
67
|
-
];
|
|
68
|
-
function isTemporaryDevServer(serverUrl) {
|
|
69
|
-
try {
|
|
70
|
-
const hostname = new URL(serverUrl).hostname.toLowerCase();
|
|
71
|
-
return hostname === "localhost"
|
|
72
|
-
|| hostname === "127.0.0.1"
|
|
73
|
-
|| hostname === "::1"
|
|
74
|
-
|| hostname === "nowwork-dev.nowcoder.com";
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function localMemoryV2Handles(env, serverUrl) {
|
|
81
|
-
const configured = env.CREW_LOCAL_MEMORY_V2_HANDLES;
|
|
82
|
-
if (configured === undefined) {
|
|
83
|
-
return isTemporaryDevServer(serverUrl)
|
|
84
|
-
? new Set(TEMP_LOCAL_MEMORY_V2_TEST_HANDLES)
|
|
85
|
-
: new Set();
|
|
86
|
-
}
|
|
87
|
-
const raw = configured.trim();
|
|
88
|
-
if (raw === "")
|
|
89
|
-
return new Set();
|
|
90
|
-
const handles = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
91
|
-
if (handles.some((handle) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(handle))) {
|
|
92
|
-
throw new ConfigError("CREW_LOCAL_MEMORY_V2_HANDLES contains an invalid agent handle");
|
|
93
|
-
}
|
|
94
|
-
return new Set(handles);
|
|
95
|
-
}
|
|
96
62
|
export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
|
|
97
63
|
function codexHomeModeEnv(env) {
|
|
98
64
|
const value = env.CREW_CODEX_HOME_MODE;
|
|
@@ -142,7 +108,6 @@ export function loadConfig(env = process.env) {
|
|
|
142
108
|
sessionMaxTurns: env.CREW_SESSION_MAX_TURNS != null ? Number(env.CREW_SESSION_MAX_TURNS) : 30,
|
|
143
109
|
productName: env.CREW_PRODUCT_NAME ?? "nowwork",
|
|
144
110
|
agentMemory: loadAgentMemoryConfig(env),
|
|
145
|
-
localMemoryV2Handles: localMemoryV2Handles(env, serverUrl),
|
|
146
111
|
executionLimits,
|
|
147
112
|
};
|
|
148
113
|
}
|
|
@@ -10,7 +10,6 @@ import { buildServiceSpec, captureExactService, serviceStatus, systemCommandRunn
|
|
|
10
10
|
import { readManagedServiceRegistry } from "./managed-service-registry.js";
|
|
11
11
|
import { uninstallManagedService } from "./managed-service-lifecycle.js";
|
|
12
12
|
import { resolveAutomaticInstallTarget } from "./automatic-install-target.js";
|
|
13
|
-
import { buildStorageMigrationMappings, createSafeServiceManifest, CredentialMaterialError, } from "./storage-migration-manifest.js";
|
|
14
13
|
const MigrationMessageSchema = z.object({
|
|
15
14
|
type: z.literal("daemon:migrate"),
|
|
16
15
|
migrationId: z.string().uuid(),
|
|
@@ -48,30 +47,33 @@ async function hashedFiles(root, directory = root) {
|
|
|
48
47
|
return files;
|
|
49
48
|
}
|
|
50
49
|
async function backupAndCopy(input, id, targetHome, targetAgents, service) {
|
|
51
|
-
// Validate and project service identity before creating any backup bytes. A descriptor containing
|
|
52
|
-
// a token must stop the migration before it can be copied into the local backup root.
|
|
53
|
-
const safeService = createSafeServiceManifest({
|
|
54
|
-
spec: service.spec,
|
|
55
|
-
snapshot: service.snapshot,
|
|
56
|
-
managedRecord: service.managedRecord,
|
|
57
|
-
});
|
|
58
50
|
const backup = migrationBackupRoot(input, id);
|
|
59
51
|
await mkdir(backup, { recursive: true, mode: 0o700 });
|
|
60
52
|
await cp(input.currentDaemonHome, resolve(backup, "daemon-home"), { recursive: true, force: false });
|
|
61
53
|
await cp(input.currentAgentsRoot, resolve(backup, "agents-root"), { recursive: true, force: false });
|
|
62
54
|
await cp(input.currentDaemonHome, targetHome, { recursive: true, force: false });
|
|
63
55
|
await cp(input.currentAgentsRoot, targetAgents, { recursive: true, force: false });
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
const files = [
|
|
57
|
+
...(await hashedFiles(resolve(backup, "daemon-home"))).map((file) => ({ ...file, path: `daemon-home/${file.path}` })),
|
|
58
|
+
...(await hashedFiles(resolve(backup, "agents-root"))).map((file) => ({ ...file, path: `agents-root/${file.path}` })),
|
|
59
|
+
];
|
|
67
60
|
await writeFile(resolve(backup, "manifest.json"), `${JSON.stringify({
|
|
68
|
-
version:
|
|
61
|
+
version: 1,
|
|
69
62
|
migrationId: id,
|
|
70
63
|
profile: input.profileName,
|
|
71
64
|
daemonHome: input.currentDaemonHome,
|
|
72
65
|
agentsRoot: input.currentAgentsRoot,
|
|
73
|
-
service:
|
|
74
|
-
|
|
66
|
+
service: {
|
|
67
|
+
id: service.spec.id,
|
|
68
|
+
platform: service.spec.platform,
|
|
69
|
+
descriptorPath: service.spec.descriptorPath,
|
|
70
|
+
descriptor: service.spec.descriptor,
|
|
71
|
+
daemonCommand: service.spec.daemonCommand,
|
|
72
|
+
installed: service.snapshot.installed,
|
|
73
|
+
running: service.snapshot.running,
|
|
74
|
+
managedRecord: service.managedRecord,
|
|
75
|
+
},
|
|
76
|
+
files,
|
|
75
77
|
}, null, 2)}\n`, { mode: 0o600 });
|
|
76
78
|
return backup;
|
|
77
79
|
}
|
|
@@ -145,7 +147,7 @@ export function createDaemonMigrationController(input) {
|
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
catch (error) {
|
|
148
|
-
sendFailure(message.migrationId,
|
|
150
|
+
sendFailure(message.migrationId, "target_conflict", error);
|
|
149
151
|
return;
|
|
150
152
|
}
|
|
151
153
|
input.sendStatus({ type: "daemon:migration-status", migrationId: message.migrationId, status: "backing_up" });
|
|
@@ -156,7 +158,7 @@ export function createDaemonMigrationController(input) {
|
|
|
156
158
|
}
|
|
157
159
|
catch (error) {
|
|
158
160
|
await Promise.allSettled([rm(targetPath, { recursive: true, force: true }), rm(targetAgents, { recursive: true, force: true })]);
|
|
159
|
-
sendFailure(message.migrationId,
|
|
161
|
+
sendFailure(message.migrationId, "backup_failed", error);
|
|
160
162
|
return;
|
|
161
163
|
}
|
|
162
164
|
const install = input.install ?? installDaemonAutomatically;
|
package/dist/execution-runner.js
CHANGED
|
@@ -847,7 +847,6 @@ export async function runExecution(config, input, dependencies) {
|
|
|
847
847
|
softTokens: config.sessionSoftTokens,
|
|
848
848
|
maxTurns: config.sessionMaxTurns,
|
|
849
849
|
},
|
|
850
|
-
localMemoryV2Enabled: config.localMemoryV2Handles?.has(spec.agent.handle) === true,
|
|
851
850
|
};
|
|
852
851
|
const result = await Promise.race([
|
|
853
852
|
execute(localInput, callbacks, localDependencies),
|
package/dist/local-executor.js
CHANGED
|
@@ -31,7 +31,6 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
|
|
|
31
31
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
32
32
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
33
33
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
34
|
-
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, renderLocalMemoryResultInstructions, } from "./local-memory/runtime.js";
|
|
35
34
|
function memoryPruneFileFactFields(fileFacts) {
|
|
36
35
|
const fields = {};
|
|
37
36
|
for (const [label, fact] of Object.entries(fileFacts)) {
|
|
@@ -278,7 +277,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
278
277
|
}, "workspace_prepare", error);
|
|
279
278
|
throw error;
|
|
280
279
|
}
|
|
281
|
-
const localMemoryV2Initialized = await initializeLocalMemoryV2ForWorkspace(workspace, input.localMemoryV2Enabled === true);
|
|
282
280
|
let materialized = null;
|
|
283
281
|
let knownAttachmentDirectory = null;
|
|
284
282
|
const startupReservationState = { current: null };
|
|
@@ -288,7 +286,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
288
286
|
let memoryPruneFailurePhase = "diagnostics_before";
|
|
289
287
|
let memoryPruneBeforeSnapshot = null;
|
|
290
288
|
let memoryPruneSharedWriteKey = null;
|
|
291
|
-
let localMemoryResultPath = null;
|
|
292
289
|
let executionWorkspace = workspace;
|
|
293
290
|
let localMemoryTelemetry = null;
|
|
294
291
|
const inheritedEnv = { ...process.env };
|
|
@@ -374,6 +371,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
374
371
|
nearBudget,
|
|
375
372
|
...(input.projectContext === undefined ? {} : { projectContext: input.projectContext }),
|
|
376
373
|
};
|
|
374
|
+
const systemPrompt = resolvePrompt(input.systemPrompt, promptContext);
|
|
377
375
|
if (input.attachments && input.attachments.length > 0) {
|
|
378
376
|
if (dependencies.cancellation?.isRequested())
|
|
379
377
|
throw new RuntimeCancelledError();
|
|
@@ -407,29 +405,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
407
405
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
408
406
|
const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
409
407
|
memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
|
|
410
|
-
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
411
|
-
&& isLocalMemoryV2Eligible({
|
|
412
|
-
enabledForHandle: true,
|
|
413
|
-
agentHomePreexisted: workspace.agentHomePreexisted === true,
|
|
414
|
-
memorySeedCreated: executionWorkspace.memorySeedCreated === true && (localMemoryV2Initialized || workspace.localMemoryV2Initialized !== false),
|
|
415
|
-
localMemoryV2Initialized,
|
|
416
|
-
memoryPrune: true,
|
|
417
|
-
})) {
|
|
418
|
-
localMemoryResultPath = makeLocalMemoryResultPath(executionWorkspace.runDir, input.executionId);
|
|
419
|
-
}
|
|
420
|
-
const baseSystemPrompt = resolvePrompt(input.systemPrompt, promptContext);
|
|
421
|
-
const structuredMemoryInstructions = localMemoryResultPath === null
|
|
422
|
-
? null
|
|
423
|
-
: renderLocalMemoryResultInstructions(localMemoryResultPath);
|
|
424
|
-
const structuredPromptFits = structuredMemoryInstructions === null
|
|
425
|
-
|| input.maxSystemPromptBytes === undefined
|
|
426
|
-
|| Buffer.byteLength(baseSystemPrompt, "utf8") + Buffer.byteLength(structuredMemoryInstructions, "utf8")
|
|
427
|
-
<= input.maxSystemPromptBytes;
|
|
428
|
-
if (!structuredPromptFits)
|
|
429
|
-
localMemoryResultPath = null;
|
|
430
|
-
const systemPrompt = localMemoryResultPath === null
|
|
431
|
-
? baseSystemPrompt
|
|
432
|
-
: `${baseSystemPrompt}${structuredMemoryInstructions ?? ""}`;
|
|
433
408
|
try {
|
|
434
409
|
localMemoryTelemetry = createLocalMemoryTelemetry({
|
|
435
410
|
executionId: input.executionId,
|
|
@@ -917,24 +892,6 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
917
892
|
const { exitCode, spawnError, terminationSignal } = runtimeExit;
|
|
918
893
|
localMemoryTelemetry?.markRuntimeExited(exitCode);
|
|
919
894
|
memoryPruneRuntimeExitCode = exitCode;
|
|
920
|
-
if (localMemoryResultPath !== null) {
|
|
921
|
-
const committed = await commitLocalMemoryResultAfterExit({
|
|
922
|
-
resultPath: localMemoryResultPath,
|
|
923
|
-
agentDir: executionWorkspace.dir,
|
|
924
|
-
executionId: input.executionId,
|
|
925
|
-
pruneTraceId: memoryPruneTraceId,
|
|
926
|
-
exitCode,
|
|
927
|
-
});
|
|
928
|
-
if (committed.status === "failed") {
|
|
929
|
-
dslog("local_memory.commit_failed", "结构化本地记忆候选未落地,保留结果文件", {
|
|
930
|
-
level: "WARN",
|
|
931
|
-
execution_id: input.executionId,
|
|
932
|
-
agent_handle: input.handle,
|
|
933
|
-
error_code: committed.error,
|
|
934
|
-
result_path: localMemoryResultPath,
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
895
|
const errorTail = [
|
|
939
896
|
stderrTail.trim(),
|
|
940
897
|
spawnError === undefined
|
package/dist/machine-info.js
CHANGED
|
@@ -191,11 +191,10 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
|
|
|
191
191
|
profileName: dependencies.profileName,
|
|
192
192
|
daemonHome: dependencies.daemonHome ?? daemonHome(),
|
|
193
193
|
agentsRoot,
|
|
194
|
-
...(dependencies.profileName.match(/^nw-(?:
|
|
194
|
+
...(dependencies.profileName.match(/^nw-(?:in|dev)-([a-z0-9][a-z0-9_-]{0,39})$/u)?.[1] === undefined
|
|
195
195
|
? {}
|
|
196
|
-
: { localInstanceName: dependencies.profileName.match(/^nw-(?:
|
|
196
|
+
: { localInstanceName: dependencies.profileName.match(/^nw-(?:in|dev)-([a-z0-9][a-z0-9_-]{0,39})$/u)[1] }),
|
|
197
197
|
}),
|
|
198
|
-
...(dependencies.storageIdentity ?? {}),
|
|
199
198
|
...(backend.supported ? {
|
|
200
199
|
executionProtocol: EXECUTION_PROTOCOL,
|
|
201
200
|
executionLimits: Object.freeze({
|
package/dist/main.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
10
|
import { loadConfig, ConfigError } from "./config.js";
|
|
12
11
|
import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
|
|
13
12
|
import { cliVersion, daemonVersion } from "./machine-info.js";
|
|
@@ -22,18 +21,7 @@ import { formatDaemonStartupError } from "./daemon-startup-error.js";
|
|
|
22
21
|
import { assertManagedServiceStartup } from "./managed-service-startup.js";
|
|
23
22
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
24
23
|
import { acquireDaemonInstallationLease, runWithDaemonInstallationLease, } from "./daemon-installation-lease.js";
|
|
25
|
-
import { loadStorageRuntime } from "./storage-runtime.js";
|
|
26
|
-
import { readStorageMigrationReceipt } from "./storage-migration-controller.js";
|
|
27
|
-
import { storageMigrationStatusReplay } from "./storage-status-replay.js";
|
|
28
|
-
import { runStorageWorkerCommand } from "./storage-worker-command.js";
|
|
29
|
-
import { probeStorageMigrationRuntime } from "./storage-runtime-readiness.js";
|
|
30
|
-
import { createStorageTargetConfirmationHandler } from "./storage-target-confirmation.js";
|
|
31
24
|
async function main() {
|
|
32
|
-
const workerResult = await runStorageWorkerCommand(process.argv.slice(2));
|
|
33
|
-
if (workerResult !== null) {
|
|
34
|
-
process.exitCode = workerResult;
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
25
|
const computerResult = await runComputerCommand(process.argv.slice(2));
|
|
38
26
|
if (computerResult !== null) {
|
|
39
27
|
process.exitCode = computerResult;
|
|
@@ -69,10 +57,8 @@ async function main() {
|
|
|
69
57
|
// 不把 machine token 放进 argv / plist / systemd unit / scheduled task。
|
|
70
58
|
if (values["daemon-home"])
|
|
71
59
|
process.env.CREW_DAEMON_HOME = values["daemon-home"];
|
|
72
|
-
let activeDaemonHome;
|
|
73
60
|
if (values.profile) {
|
|
74
61
|
const home = daemonHome();
|
|
75
|
-
activeDaemonHome = home;
|
|
76
62
|
const profile = await loadProfile(values.profile, home);
|
|
77
63
|
if (cmd === "serve") {
|
|
78
64
|
await assertProfileAgentsRootUnique(profile, home, homedir());
|
|
@@ -114,72 +100,9 @@ async function main() {
|
|
|
114
100
|
const installationLease = installation === null
|
|
115
101
|
? null
|
|
116
102
|
: await acquireDaemonInstallationLease(installation.npmPrefix);
|
|
117
|
-
const storageRuntime = values.profile === undefined
|
|
118
|
-
? null
|
|
119
|
-
: await loadStorageRuntime({
|
|
120
|
-
profile: values.profile,
|
|
121
|
-
serverUrl: config.serverUrl,
|
|
122
|
-
userHome: homedir(),
|
|
123
|
-
platform: process.platform,
|
|
124
|
-
environment: process.env,
|
|
125
|
-
});
|
|
126
|
-
const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
|
|
127
|
-
? { ready: false, reason: "not_legacy_source" }
|
|
128
|
-
: await probeStorageMigrationRuntime({
|
|
129
|
-
profile: values.profile,
|
|
130
|
-
serverUrl: config.serverUrl,
|
|
131
|
-
currentDaemonHome: activeDaemonHome,
|
|
132
|
-
currentAgentsRoot: config.agentsRoot,
|
|
133
|
-
userHome: homedir(),
|
|
134
|
-
platform: process.platform,
|
|
135
|
-
...(process.getuid === undefined ? {} : { uid: process.getuid() }),
|
|
136
|
-
environment: process.env,
|
|
137
|
-
});
|
|
138
|
-
const targetConfirmation = storageRuntime?.identity.storageMigrationId === undefined
|
|
139
|
-
? null
|
|
140
|
-
: createStorageTargetConfirmationHandler({
|
|
141
|
-
backupsRoot: storageRuntime.local.backupsRoot,
|
|
142
|
-
identity: storageRuntime.identity,
|
|
143
|
-
profile: storageRuntime.installation.profile,
|
|
144
|
-
completionTimeoutMs: 180_000,
|
|
145
|
-
});
|
|
146
|
-
const storageMigration = storageRuntime !== null
|
|
147
|
-
? {
|
|
148
|
-
ready: false,
|
|
149
|
-
statusOutboxPath: join(storageRuntime.local.stateRoot, "storage-status-outbox.json"),
|
|
150
|
-
identity: storageRuntime.identity,
|
|
151
|
-
...(targetConfirmation === null ? {} : { confirm: targetConfirmation.handle }),
|
|
152
|
-
...(storageRuntime.identity.storageMigrationId === undefined ? {} : {
|
|
153
|
-
replayStatus: async (send) => {
|
|
154
|
-
const migrationId = storageRuntime.identity.storageMigrationId;
|
|
155
|
-
const receipt = await readStorageMigrationReceipt(storageRuntime.local.backupsRoot, migrationId);
|
|
156
|
-
if (receipt === null)
|
|
157
|
-
throw new Error(`storage migration receipt not found: ${migrationId}`);
|
|
158
|
-
for (const frame of storageMigrationStatusReplay(receipt, {
|
|
159
|
-
migrationId,
|
|
160
|
-
environment: storageRuntime.identity.storageEnvironment,
|
|
161
|
-
workspaceId: storageRuntime.identity.storageWorkspaceId,
|
|
162
|
-
profile: storageRuntime.installation.profile,
|
|
163
|
-
}))
|
|
164
|
-
send(frame);
|
|
165
|
-
},
|
|
166
|
-
}),
|
|
167
|
-
}
|
|
168
|
-
: migrationRuntime.ready
|
|
169
|
-
? {
|
|
170
|
-
ready: true,
|
|
171
|
-
statusOutboxPath: migrationRuntime.runtime.statusOutboxPath,
|
|
172
|
-
identity: migrationRuntime.runtime.identity,
|
|
173
|
-
controller: migrationRuntime.runtime.controller,
|
|
174
|
-
attach: migrationRuntime.runtime.attach,
|
|
175
|
-
bindServerIdentity: migrationRuntime.runtime.bindServerIdentity,
|
|
176
|
-
replayStatus: migrationRuntime.runtime.replayStatus,
|
|
177
|
-
}
|
|
178
|
-
: undefined;
|
|
179
103
|
await runWithDaemonInstallationLease(installationLease, async () => {
|
|
180
104
|
const service = serve(config, {
|
|
181
105
|
...(values.profile === undefined ? {} : { profileName: values.profile }),
|
|
182
|
-
...(storageMigration === undefined ? {} : { storageMigration }),
|
|
183
106
|
});
|
|
184
107
|
await runServeLifecycle(service);
|
|
185
108
|
});
|
package/dist/profile-layout.js
CHANGED
|
@@ -2,10 +2,6 @@ import { posix, win32 } from "node:path";
|
|
|
2
2
|
export const PROFILE_LAYOUT_VERSION = 1;
|
|
3
3
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
|
|
4
4
|
const LOCAL_INSTANCE_NAME = /^[a-z0-9][a-z0-9_-]{0,39}$/u;
|
|
5
|
-
/** Accept the legacy `in` tag at the boundary, but never persist it in a new layout. */
|
|
6
|
-
export function normalizeProfileEnvironment(value) {
|
|
7
|
-
return value === "in" ? "production" : value;
|
|
8
|
-
}
|
|
9
5
|
export function validateLocalInstanceName(value) {
|
|
10
6
|
const normalized = value.trim().toLowerCase();
|
|
11
7
|
if (!LOCAL_INSTANCE_NAME.test(normalized) || normalized.includes("..")) {
|
|
@@ -14,21 +10,19 @@ export function validateLocalInstanceName(value) {
|
|
|
14
10
|
return normalized;
|
|
15
11
|
}
|
|
16
12
|
export function profileForLocalInstance(environment, localInstanceName) {
|
|
17
|
-
const
|
|
18
|
-
const profile = `nw-${canonicalEnvironment}-${validateLocalInstanceName(localInstanceName)}`;
|
|
13
|
+
const profile = `nw-${environment}-${validateLocalInstanceName(localInstanceName)}`;
|
|
19
14
|
if (!PROFILE_NAME.test(profile))
|
|
20
15
|
throw new Error("derived daemon profile exceeds 48 characters");
|
|
21
16
|
return profile;
|
|
22
17
|
}
|
|
23
18
|
export function profileLayout(input) {
|
|
24
|
-
const environment = normalizeProfileEnvironment(input.environment);
|
|
25
19
|
const localInstanceName = validateLocalInstanceName(input.localInstanceName);
|
|
26
|
-
const profile = profileForLocalInstance(environment, localInstanceName);
|
|
20
|
+
const profile = profileForLocalInstance(input.environment, localInstanceName);
|
|
27
21
|
const paths = input.platform === "win32" ? win32 : posix;
|
|
28
|
-
const base = `${environment}-${localInstanceName}`;
|
|
22
|
+
const base = `${input.environment}-${localInstanceName}`;
|
|
29
23
|
return Object.freeze({
|
|
30
24
|
layoutVersion: PROFILE_LAYOUT_VERSION,
|
|
31
|
-
environment,
|
|
25
|
+
environment: input.environment,
|
|
32
26
|
localInstanceName,
|
|
33
27
|
profile,
|
|
34
28
|
daemonHome: paths.join(input.userHome, ".crew", `daemon-${base}`),
|
package/dist/runner.js
CHANGED
|
@@ -105,7 +105,6 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
105
105
|
softTokens: config.sessionSoftTokens,
|
|
106
106
|
maxTurns: config.sessionMaxTurns,
|
|
107
107
|
},
|
|
108
|
-
localMemoryV2Enabled: config.localMemoryV2Handles?.has(input.handle) === true,
|
|
109
108
|
}, {
|
|
110
109
|
onActivity,
|
|
111
110
|
onConsole,
|
package/dist/serve.js
CHANGED
|
@@ -51,7 +51,6 @@ import { handleRuntimeProbeFrame, probeRuntimeHealth } from "./runtime-health.js
|
|
|
51
51
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
52
52
|
import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
|
|
53
53
|
import { createCodexHomeModeController } from "./codex-home-mode-controller.js";
|
|
54
|
-
import { attachStorageMigrationRuntime, createServeStorageStatusDelivery, handleStorageMigrationControl, replayStorageMigrationStatus, storageMigrationCapabilities, } from "./storage-serve-adapter.js";
|
|
55
54
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
56
55
|
// normalize.ts 的活动种类 → activity 枚举
|
|
57
56
|
const ACTIVITY_MAP = {
|
|
@@ -176,36 +175,13 @@ export function serve(config, opts = {}) {
|
|
|
176
175
|
});
|
|
177
176
|
const machineHelloPublisher = createProjectSkillMachineHelloPublisher({
|
|
178
177
|
status: projectSkillControllerReadiness.status,
|
|
179
|
-
effective:
|
|
180
|
-
const effective = withProjectSkillCapabilityStatus(hello, status);
|
|
181
|
-
const configuredIdentity = opts.storageMigration?.identity;
|
|
182
|
-
const currentIdentity = typeof configuredIdentity === "function"
|
|
183
|
-
? configuredIdentity()
|
|
184
|
-
: configuredIdentity;
|
|
185
|
-
return currentIdentity === undefined ? effective : { ...effective, ...currentIdentity };
|
|
186
|
-
},
|
|
178
|
+
effective: withProjectSkillCapabilityStatus,
|
|
187
179
|
onSent: (hello) => log(`📤 已上报机器信息: ${hello.hostname} · ${hello.os} · installed=[${hello.runtimes.join(",")}] · executable=[${hello.executionRuntimes.join(",")}] · agents=[${hello.agentHandles.join(",")}]`),
|
|
188
180
|
});
|
|
189
181
|
const initializedProjectSkills = initializedProjectSkillsReconciler(projectSkillControllerReadiness.ensure, projectSkillsReconciler);
|
|
190
|
-
const configuredCapabilities = opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform);
|
|
191
|
-
const storageCapabilities = storageMigrationCapabilities(configuredCapabilities, opts.storageMigration);
|
|
192
|
-
const storageStatusDelivery = createServeStorageStatusDelivery(opts.storageMigration, {
|
|
193
|
-
onPersistenceError: (error) => dslog("daemon.storage_status_outbox_failed", "存储迁移状态 outbox 持久化失败", { level: "ERROR", error_message: error.message }),
|
|
194
|
-
transmit: (frame) => { try {
|
|
195
|
-
if (ws?.readyState === WebSocket.OPEN)
|
|
196
|
-
ws.send(JSON.stringify(frame));
|
|
197
|
-
}
|
|
198
|
-
catch { /* retry */ } },
|
|
199
|
-
});
|
|
200
|
-
const storageMigrationRuntime = attachStorageMigrationRuntime(opts.storageMigration, {
|
|
201
|
-
isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
|
|
202
|
-
waitForExecutionFrames: () => executionFrameQueue,
|
|
203
|
-
flushLocalState: flushSlog,
|
|
204
|
-
sendStatus: storageStatusDelivery.send,
|
|
205
|
-
});
|
|
206
182
|
const prepareProjectSkillCapabilities = createProjectSkillConnectionCapabilityProvider({
|
|
207
183
|
platform: process.platform,
|
|
208
|
-
baseCapabilities:
|
|
184
|
+
baseCapabilities: opts.machineInfo?.capabilities ?? daemonCapabilities(process.platform),
|
|
209
185
|
additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
|
|
210
186
|
probeRuntimeRootStorePrimitive: projectSkillRuntimeRootStartup.probeNativePublicationPrimitiveReadiness,
|
|
211
187
|
initializeController: projectSkillControllerReadiness.ensure,
|
|
@@ -334,12 +310,6 @@ export function serve(config, opts = {}) {
|
|
|
334
310
|
// 连上了才有机会把离线期间(断连原因/退出前)落盘的日志补传上去
|
|
335
311
|
void drainSpool();
|
|
336
312
|
// 上报本机信息 (hostname/os/daemon 版本/已装 runtimes)
|
|
337
|
-
storageStatusDelivery.replayPending();
|
|
338
|
-
const storageReplay = replayStorageMigrationStatus(opts.storageMigration, openedSocket, () => ws === openedSocket, (error) => {
|
|
339
|
-
dslog("daemon.storage_status_replay_failed", "存储迁移状态重放失败", {
|
|
340
|
-
level: "ERROR", error_message: error.message,
|
|
341
|
-
});
|
|
342
|
-
}, storageStatusDelivery.send);
|
|
343
313
|
const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
|
|
344
314
|
const helloPromise = (opts.machineInfo?.collect ?? collectMachineHello)(config.agentsRoot, config.executionLimits, process.platform, {
|
|
345
315
|
...(detectInstalled ? { detectInstalled } : {}),
|
|
@@ -355,7 +325,6 @@ export function serve(config, opts = {}) {
|
|
|
355
325
|
if (hello.executionProtocol === undefined)
|
|
356
326
|
return [];
|
|
357
327
|
const detected = await runtimeProbe.detect(hello.runtimes, detectExecutable);
|
|
358
|
-
await storageReplay;
|
|
359
328
|
if (ws === openedSocket && openedSocket.readyState === WebSocket.OPEN
|
|
360
329
|
&& detected.some((runtime) => !hello.executionRuntimes.includes(runtime))) {
|
|
361
330
|
detectedExecutionRuntimes = detected;
|
|
@@ -370,8 +339,7 @@ export function serve(config, opts = {}) {
|
|
|
370
339
|
return [];
|
|
371
340
|
});
|
|
372
341
|
void helloPromise
|
|
373
|
-
.then(
|
|
374
|
-
await storageReplay;
|
|
342
|
+
.then((hello) => {
|
|
375
343
|
if (ws !== openedSocket || openedSocket.readyState !== WebSocket.OPEN)
|
|
376
344
|
return;
|
|
377
345
|
detectedExecutionRuntimes = hello.executionRuntimes;
|
|
@@ -419,8 +387,6 @@ export function serve(config, opts = {}) {
|
|
|
419
387
|
if (typeof decoded !== "object" || decoded === null)
|
|
420
388
|
return;
|
|
421
389
|
const rawType = "type" in decoded && typeof decoded.type === "string" ? decoded.type : "";
|
|
422
|
-
if (storageStatusDelivery.acknowledge(decoded))
|
|
423
|
-
return;
|
|
424
390
|
if (rawType === "daemon:update" || rawType === "daemon:restart") {
|
|
425
391
|
void updateController.handle(decoded).catch((error) => {
|
|
426
392
|
dslog("daemon.control_failed", "daemon 控制操作处理失败", {
|
|
@@ -437,11 +403,6 @@ export function serve(config, opts = {}) {
|
|
|
437
403
|
void codexHomeModeController.handle(decoded).catch((error) => dslog("daemon.codex_home_mode_failed", "Codex Home 模式切换失败", { level: "ERROR", error_message: error.message }));
|
|
438
404
|
return;
|
|
439
405
|
}
|
|
440
|
-
if (handleStorageMigrationControl(opts.storageMigration, rawType, decoded, storageStatusDelivery.send, (kind, error) => {
|
|
441
|
-
dslog(kind === "migration" ? "daemon.storage_migration_failed" : "daemon.storage_migration_confirmation_failed", kind === "migration" ? "存储迁移控制操作失败" : "存储迁移 hello 确认处理失败", { level: "ERROR", error_message: error.message });
|
|
442
|
-
})) {
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
406
|
if (rawType.startsWith("execution:")) {
|
|
446
407
|
const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
|
|
447
408
|
if (!parsedExecution.success) {
|
|
@@ -503,14 +464,6 @@ export function serve(config, opts = {}) {
|
|
|
503
464
|
return;
|
|
504
465
|
}
|
|
505
466
|
const spec = frame;
|
|
506
|
-
if (storageMigrationRuntime.isAdmissionClosed()) {
|
|
507
|
-
safeExecutionSend(ExecutionRejectedSchema.parse({
|
|
508
|
-
type: "execution:rejected", protocolVersion: 1, executionId: spec.executionId,
|
|
509
|
-
reason: "resource_limit", message: "Storage migration is quiescing this machine",
|
|
510
|
-
at: new Date().toISOString(),
|
|
511
|
-
}));
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
467
|
const workspaceRejection = projectWorkspaceCapabilityRejection(spec, capabilities.executionRunner, new Date().toISOString());
|
|
515
468
|
if (workspaceRejection !== null)
|
|
516
469
|
return void safeExecutionSend(workspaceRejection);
|
|
@@ -817,17 +770,6 @@ export function serve(config, opts = {}) {
|
|
|
817
770
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
818
771
|
const r = msg;
|
|
819
772
|
setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
|
|
820
|
-
if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string") {
|
|
821
|
-
try {
|
|
822
|
-
opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId });
|
|
823
|
-
machineHelloPublisher.republish();
|
|
824
|
-
}
|
|
825
|
-
catch (error) {
|
|
826
|
-
dslog("daemon.storage_identity_bind_failed", "存储迁移 Workspace 身份绑定失败", {
|
|
827
|
-
level: "ERROR", error_message: error.message,
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
773
|
// 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。
|
|
832
774
|
serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
|
|
833
775
|
? r.serverCapabilities.filter((c) => typeof c === "string")
|
|
@@ -926,8 +868,6 @@ export function serve(config, opts = {}) {
|
|
|
926
868
|
}
|
|
927
869
|
if (msg.type !== "agent:start")
|
|
928
870
|
return; // ready/error 等忽略
|
|
929
|
-
if (storageMigrationRuntime.isAdmissionClosed())
|
|
930
|
-
return;
|
|
931
871
|
// 任务键:scheduled run 用 runId(每次运行独立 cwd/work-log;overlap 由 server 端 skip_if_running 管,
|
|
932
872
|
// daemon 的 running 去重只兜底"同一 run 重复投递");普通唤醒仍是 线程锚点 ?? 频道。
|
|
933
873
|
const scheduled = msg.reason === "scheduled_job" && msg.scheduledRun
|
|
@@ -1343,7 +1283,6 @@ export function serve(config, opts = {}) {
|
|
|
1343
1283
|
concurrencyTelemetry.stop();
|
|
1344
1284
|
runtimeProbe.stop();
|
|
1345
1285
|
completionRetransmitter.stop();
|
|
1346
|
-
storageStatusDelivery.stop();
|
|
1347
1286
|
const deadline = createShutdownDeadline(shutdownTimeoutMs);
|
|
1348
1287
|
if (reconnectTimer !== null) {
|
|
1349
1288
|
clearTimeout(reconnectTimer);
|