@nowcrew/daemon 0.6.77 → 0.6.78
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 +27 -0
- package/dist/automatic-daemon-installer.js +44 -24
- package/dist/automatic-install-target.js +34 -9
- package/dist/computer-cli.js +43 -14
- package/dist/computer-profile.js +15 -2
- package/dist/computer-service.js +26 -5
- package/dist/config.js +28 -8
- package/dist/control-plane-url.js +3 -9
- package/dist/daemon-installation-lease.js +7 -4
- package/dist/daemon-local-home.js +108 -0
- package/dist/daemon-migration-controller.js +15 -4
- package/dist/daemon-registry.js +20 -0
- package/dist/daemon-update-controller.js +1 -0
- package/dist/daemon-update-eligibility.js +17 -7
- package/dist/daemon-updater.js +26 -26
- package/dist/execution-runner.js +7 -7
- package/dist/host-execution-coordinator.js +4 -3
- package/dist/local-executor.js +23 -17
- package/dist/local-memory/runtime.js +5 -3
- package/dist/machine-info.js +3 -0
- package/dist/main.js +13 -2
- package/dist/managed-service-diagnostics.js +1 -0
- package/dist/managed-service-registry.js +12 -7
- package/dist/managed-service-startup.js +4 -1
- package/dist/npm-command.js +9 -0
- package/dist/project-skills/controller.js +6 -71
- package/dist/project-skills/execution-adapter.js +7 -0
- package/dist/project-skills/execution-projection.js +163 -0
- package/dist/project-skills/initialized-reconciler.js +10 -8
- package/dist/project-skills/legacy-cleanup.js +224 -0
- package/dist/project-skills/machine-hello-publisher.js +20 -0
- package/dist/project-skills/reconciler.js +114 -500
- package/dist/project-skills/resolution-records.js +63 -0
- package/dist/project-skills/runtime-launch.js +0 -42
- package/dist/project-skills/runtime-warning.js +1 -3
- package/dist/project-skills/scanner.js +1 -17
- package/dist/project-skills/startup-cleanup.js +24 -0
- package/dist/runner.js +2 -1
- package/dist/serve.js +49 -49
- package/dist/slog.js +14 -1
- package/dist/storage-control.js +12 -1
- package/dist/storage-identity.js +15 -1
- package/dist/storage-migration-controller.js +25 -4
- package/dist/storage-ownership.js +55 -0
- package/dist/storage-runtime-controller.js +29 -7
- package/dist/storage-runtime-readiness.js +26 -4
- package/dist/storage-runtime.js +18 -2
- package/dist/storage-status-delivery.js +4 -0
- package/dist/storage-status-replay.js +16 -1
- package/dist/storage-worker-executor.js +1 -0
- package/dist/storage-worker-operation.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 新 daemon 首次启动时清掉旧实现留下的状态:
|
|
3
|
+
* - <agent>/.crew/project-skill-runtime/(版本化根 + 租约 + 索引):lease 全部终态或超龄则整目录删除;
|
|
4
|
+
* - 共享目录 .agents/skills 与 .crew/claude-skills/.claude/skills 里的项目 skill 投影(training / ability 链接保留);
|
|
5
|
+
* - 旧 applied manifest 与切换 journal。
|
|
6
|
+
* 只在 journal reconcile 之后运行;失败只记日志,不影响能力。
|
|
7
|
+
*/
|
|
8
|
+
import { lstat, readdir, readFile, readlink, rm } from "node:fs/promises";
|
|
9
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { isManagedDirectoryProjectionCopy } from "../directory-projection.js";
|
|
11
|
+
import { sweepExecutionProjections } from "./execution-projection.js";
|
|
12
|
+
export const LEGACY_RUNTIME_STORE = Object.freeze([".crew", "project-skill-runtime"]);
|
|
13
|
+
export const LEGACY_SHARED_SKILL_DIRS = Object.freeze([
|
|
14
|
+
Object.freeze([".agents", "skills"]),
|
|
15
|
+
Object.freeze([".crew", "claude-skills", ".claude", "skills"]),
|
|
16
|
+
]);
|
|
17
|
+
const LEGACY_FILES = Object.freeze([
|
|
18
|
+
Object.freeze([".crew", "project-skills-v2.json"]),
|
|
19
|
+
Object.freeze([".crew", "project-skills-switch.json"]),
|
|
20
|
+
]);
|
|
21
|
+
const LEGACY_ROOT_MARKER = ".nowcrew-project-skills-root.json";
|
|
22
|
+
const LEGACY_STAGING = /^\.(skills|ability-skills)-(next|previous|discard)-/u;
|
|
23
|
+
const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
24
|
+
const isWithin = (root, candidate) => candidate === root || candidate.startsWith(`${root}${sep}`);
|
|
25
|
+
const agentDirectories = async (agentsRoot) => {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = await readdir(agentsRoot, { withFileTypes: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
34
|
+
};
|
|
35
|
+
const leaseExecutionIds = async (leasesDir) => {
|
|
36
|
+
let names;
|
|
37
|
+
try {
|
|
38
|
+
names = await readdir(leasesDir);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const ids = [];
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(await readFile(join(leasesDir, name), "utf8"));
|
|
47
|
+
if (typeof parsed.executionId === "string")
|
|
48
|
+
ids.push(parsed.executionId);
|
|
49
|
+
}
|
|
50
|
+
catch { /* 读不出来视为无主 */ }
|
|
51
|
+
}
|
|
52
|
+
return ids;
|
|
53
|
+
};
|
|
54
|
+
const newestMtime = async (path) => {
|
|
55
|
+
let newest = 0;
|
|
56
|
+
const walk = async (current) => {
|
|
57
|
+
const info = await lstat(current).catch(() => null);
|
|
58
|
+
if (info === null)
|
|
59
|
+
return;
|
|
60
|
+
newest = Math.max(newest, info.mtimeMs);
|
|
61
|
+
if (info.isDirectory() && !info.isSymbolicLink()) {
|
|
62
|
+
for (const entry of await readdir(current).catch(() => []))
|
|
63
|
+
await walk(join(current, entry));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
await walk(path);
|
|
67
|
+
return newest;
|
|
68
|
+
};
|
|
69
|
+
const isTerminal = async (journal, executionId) => {
|
|
70
|
+
const entry = await journal.get(executionId).catch(() => null);
|
|
71
|
+
return entry === null || entry.state === "completed" || entry.state === "interrupted";
|
|
72
|
+
};
|
|
73
|
+
const ownedByCurrentUser = (uid) => process.getuid === undefined || uid === process.getuid();
|
|
74
|
+
/**
|
|
75
|
+
* agentRoot 到 directory 的每一层都必须是本用户的真实目录。任一层缺失 / 是 symlink / 不是目录 /
|
|
76
|
+
* 属于他人 → false(跳过这个目录),绝不抛:清理一个 Agent 失败不该影响其他 Agent,
|
|
77
|
+
* 更不该让 readdir/rm 顺着换进来的 symlink 删到托管树之外。
|
|
78
|
+
*/
|
|
79
|
+
const isManagedChain = async (agentRoot, directory) => {
|
|
80
|
+
const rel = relative(agentRoot, directory);
|
|
81
|
+
if (isAbsolute(rel) || rel.split(sep).includes(".."))
|
|
82
|
+
return false;
|
|
83
|
+
const paths = [agentRoot];
|
|
84
|
+
let current = agentRoot;
|
|
85
|
+
for (const segment of rel === "" ? [] : rel.split(sep)) {
|
|
86
|
+
current = join(current, segment);
|
|
87
|
+
paths.push(current);
|
|
88
|
+
}
|
|
89
|
+
for (const path of paths) {
|
|
90
|
+
const info = await lstat(path).catch(() => null);
|
|
91
|
+
if (info === null || info.isSymbolicLink() || !info.isDirectory() || !ownedByCurrentUser(info.uid)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
/** 旧切换实现的 staging/backup/discard 目录是 skills 的兄弟(不是它的子项)。 */
|
|
98
|
+
const removeStagingSiblings = async (parent) => {
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = await readdir(parent, { withFileTypes: true });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
let removed = 0;
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!LEGACY_STAGING.test(entry.name))
|
|
109
|
+
continue;
|
|
110
|
+
const path = join(parent, entry.name);
|
|
111
|
+
const info = await lstat(path).catch(() => null);
|
|
112
|
+
if (info === null || info.isSymbolicLink() || !info.isDirectory())
|
|
113
|
+
continue;
|
|
114
|
+
await rm(path, { recursive: true, force: true });
|
|
115
|
+
removed += 1;
|
|
116
|
+
}
|
|
117
|
+
return removed;
|
|
118
|
+
};
|
|
119
|
+
const cleanSharedDirectory = async (agentRoot, directory, platform) => {
|
|
120
|
+
const parent = dirname(directory);
|
|
121
|
+
let removed = (await isManagedChain(agentRoot, parent)) ? await removeStagingSiblings(parent) : 0;
|
|
122
|
+
if (!await isManagedChain(agentRoot, directory))
|
|
123
|
+
return removed;
|
|
124
|
+
const keepRoots = [join(agentRoot, "training", "skills"), join(agentRoot, ".nowcrew", "ability", "releases")];
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return removed;
|
|
131
|
+
}
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const path = join(directory, entry.name);
|
|
134
|
+
if (entry.name === LEGACY_ROOT_MARKER || LEGACY_STAGING.test(entry.name)) {
|
|
135
|
+
await rm(path, { recursive: true, force: true });
|
|
136
|
+
removed += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (entry.isSymbolicLink()) {
|
|
140
|
+
const target = resolve(dirname(path), await readlink(path).catch(() => ""));
|
|
141
|
+
if (keepRoots.some((root) => isWithin(root, target)))
|
|
142
|
+
continue;
|
|
143
|
+
await rm(path, { force: true });
|
|
144
|
+
removed += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (entry.isDirectory() && platform === "win32" && await isManagedDirectoryProjectionCopy(path, platform)) {
|
|
148
|
+
await rm(path, { recursive: true, force: true });
|
|
149
|
+
removed += 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return removed;
|
|
153
|
+
};
|
|
154
|
+
export async function cleanupLegacyProjectSkillState(input) {
|
|
155
|
+
const platform = input.platform ?? process.platform;
|
|
156
|
+
const now = input.now ?? Date.now;
|
|
157
|
+
const maxAgeMs = input.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
158
|
+
const removedStores = [];
|
|
159
|
+
const keptStores = [];
|
|
160
|
+
const failedAgents = [];
|
|
161
|
+
let removedSharedEntries = 0;
|
|
162
|
+
for (const handle of await agentDirectories(input.agentsRoot)) {
|
|
163
|
+
const agentRoot = join(input.agentsRoot, handle);
|
|
164
|
+
try {
|
|
165
|
+
const store = join(agentRoot, ...LEGACY_RUNTIME_STORE);
|
|
166
|
+
// 托管链先行:`.crew` 被换成 symlink 时 store 会落到托管树之外,那里的空 leases 目录会让
|
|
167
|
+
// `[].every` 判定为"全终态",于是 rm -rf 删到别人的目录里去。链不成立 → 整个 store 块跳过。
|
|
168
|
+
const storeInfo = (await isManagedChain(agentRoot, dirname(store)))
|
|
169
|
+
? await lstat(store).catch(() => null)
|
|
170
|
+
: null;
|
|
171
|
+
if (storeInfo?.isDirectory() === true && !storeInfo.isSymbolicLink()) {
|
|
172
|
+
const ids = await leaseExecutionIds(join(store, "leases"));
|
|
173
|
+
const terminal = (await Promise.all(ids.map((id) => isTerminal(input.journal, id)))).every(Boolean);
|
|
174
|
+
// 全终态就不必再走整棵树 stat 了(newestMtime 是深度遍历)。
|
|
175
|
+
if (terminal || now() - await newestMtime(store) > maxAgeMs) {
|
|
176
|
+
await rm(store, { recursive: true, force: true });
|
|
177
|
+
removedStores.push(handle);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
keptStores.push(handle);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const parts of LEGACY_SHARED_SKILL_DIRS) {
|
|
184
|
+
removedSharedEntries += await cleanSharedDirectory(agentRoot, join(agentRoot, ...parts), platform);
|
|
185
|
+
}
|
|
186
|
+
for (const parts of LEGACY_FILES) {
|
|
187
|
+
const path = join(agentRoot, ...parts);
|
|
188
|
+
if (!await isManagedChain(agentRoot, dirname(path)))
|
|
189
|
+
continue;
|
|
190
|
+
if ((await lstat(path).catch(() => null)) !== null) {
|
|
191
|
+
await rm(path, { force: true });
|
|
192
|
+
removedSharedEntries += 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// 一个 Agent 的旧状态清不掉,只记一笔继续清下一个 —— 否则第一个坏目录会让每次开机都清不干净。
|
|
198
|
+
failedAgents.push(handle);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
removedStores: Object.freeze(removedStores),
|
|
203
|
+
keptStores: Object.freeze(keptStores),
|
|
204
|
+
removedSharedEntries,
|
|
205
|
+
failedAgents: Object.freeze(failedAgents),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
export async function sweepAllExecutionProjections(agentsRoot, isLive) {
|
|
209
|
+
let removed = 0;
|
|
210
|
+
let kept = 0;
|
|
211
|
+
let failed = 0;
|
|
212
|
+
for (const handle of await agentDirectories(agentsRoot)) {
|
|
213
|
+
try {
|
|
214
|
+
const result = await sweepExecutionProjections(join(agentsRoot, handle), isLive);
|
|
215
|
+
removed += result.removed.length;
|
|
216
|
+
kept += result.kept.length;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// 例如 unmanaged_path:这个 Agent 的投影父目录被换成了 symlink。跳过它,别拖累其他 Agent。
|
|
220
|
+
failed += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return Object.freeze({ removed, kept, failed });
|
|
224
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
/** 记住最近一次 hello 与 socket,便于存储身份变化后重发;Project Skills 状态恒为 ready。 */
|
|
3
|
+
export function createMachineHelloPublisher(input) {
|
|
4
|
+
let latest = null;
|
|
5
|
+
let latestSocket = null;
|
|
6
|
+
const republish = () => {
|
|
7
|
+
if (latest === null || latestSocket?.readyState !== WebSocket.OPEN)
|
|
8
|
+
return;
|
|
9
|
+
const hello = input.effective({ ...latest, projectSkillsStatus: "ready" });
|
|
10
|
+
try {
|
|
11
|
+
latestSocket.send(JSON.stringify(hello));
|
|
12
|
+
input.onSent(hello);
|
|
13
|
+
}
|
|
14
|
+
catch { /* 非 OPEN,忽略 */ }
|
|
15
|
+
};
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
publish: (hello, socket) => { latest = hello; latestSocket = socket; republish(); },
|
|
18
|
+
republish,
|
|
19
|
+
});
|
|
20
|
+
}
|