@nowcrew/daemon 0.5.34 → 0.5.36
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 +46 -0
- package/dist/atomic-private-write.js +18 -0
- package/dist/computer-profile.js +3 -17
- package/dist/execution-protocol.js +7 -1
- package/dist/execution-runner.js +2 -0
- package/dist/local-executor.js +65 -2
- package/dist/machine-info.js +5 -1
- package/dist/memory-prune-diagnostics.js +57 -0
- package/dist/project-skills/agent-projection-coordinator.js +10 -0
- package/dist/project-skills/controller.js +167 -0
- package/dist/project-skills/reconciler.js +116 -0
- package/dist/project-skills/registry.js +113 -0
- package/dist/project-skills/scanner.js +126 -0
- package/dist/project-skills/types.js +12 -0
- package/dist/promise-tail.js +25 -0
- package/dist/runner.js +4 -0
- package/dist/runtimes/claude.js +2 -0
- package/dist/runtimes/codex-app-server-runner.js +10 -1
- package/dist/scheduled-report.js +3 -0
- package/dist/serve.js +188 -10
- package/dist/skill-frontmatter.js +23 -0
- package/dist/skills.js +2 -19
- package/dist/supervised-runtime.js +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { lstat, mkdir, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export class ProjectProjectionError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code) {
|
|
7
|
+
super(code);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "ProjectProjectionError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const exists = async (path) => lstat(path).then(() => true, () => false);
|
|
13
|
+
async function switchProjectionSet(targets) {
|
|
14
|
+
const moved = [];
|
|
15
|
+
try {
|
|
16
|
+
for (const target of targets) {
|
|
17
|
+
const hadPrevious = await exists(target.target);
|
|
18
|
+
if (hadPrevious)
|
|
19
|
+
await rename(target.target, target.backup);
|
|
20
|
+
const state = { target, hadPrevious, activated: false };
|
|
21
|
+
moved.push(state);
|
|
22
|
+
await rename(target.staging, target.target);
|
|
23
|
+
state.activated = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
for (const state of [...moved].reverse()) {
|
|
28
|
+
if (state.activated)
|
|
29
|
+
await rm(state.target.target, { recursive: true, force: true }).catch(() => { });
|
|
30
|
+
if (state.hadPrevious)
|
|
31
|
+
await rename(state.target.backup, state.target.target).catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
await Promise.all(targets.map((target) => rm(target.backup, { recursive: true, force: true })));
|
|
36
|
+
}
|
|
37
|
+
export function createProjectSkillsReconciler(deps) {
|
|
38
|
+
const platform = deps.platform ?? process.platform;
|
|
39
|
+
const reconcileUnlocked = async (handle, bindings) => {
|
|
40
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
41
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
42
|
+
}
|
|
43
|
+
const projects = deps.scannedProjects();
|
|
44
|
+
const uniqueBindings = [...new Map(bindings.map((binding) => [
|
|
45
|
+
`${binding.projectId}\0${binding.skillName}`,
|
|
46
|
+
binding,
|
|
47
|
+
])).values()].sort((left, right) => left.projectId.localeCompare(right.projectId) || left.skillName.localeCompare(right.skillName));
|
|
48
|
+
const names = new Map();
|
|
49
|
+
for (const binding of uniqueBindings) {
|
|
50
|
+
const owner = names.get(binding.skillName);
|
|
51
|
+
if (owner !== undefined && owner !== binding.projectId) {
|
|
52
|
+
throw new ProjectProjectionError("skill_name_conflict");
|
|
53
|
+
}
|
|
54
|
+
names.set(binding.skillName, binding.projectId);
|
|
55
|
+
}
|
|
56
|
+
const linked = [];
|
|
57
|
+
const resolutions = [];
|
|
58
|
+
for (const binding of uniqueBindings) {
|
|
59
|
+
const project = projects.find((candidate) => candidate.inventory.projectId === binding.projectId
|
|
60
|
+
&& candidate.inventory.status === "available");
|
|
61
|
+
const skill = project?.resolved.find((candidate) => candidate.name === binding.skillName);
|
|
62
|
+
const available = skill !== undefined && (await stat(skill.sourcePath).catch(() => null))?.isDirectory() === true;
|
|
63
|
+
resolutions.push(Object.freeze({ ...binding, status: available ? "linked" : "unavailable" }));
|
|
64
|
+
if (available && skill !== undefined)
|
|
65
|
+
linked.push({ binding, skill });
|
|
66
|
+
}
|
|
67
|
+
const agentRoot = join(deps.agentsRoot, handle);
|
|
68
|
+
const id = randomUUID();
|
|
69
|
+
const targets = [
|
|
70
|
+
{
|
|
71
|
+
runtime: "codex",
|
|
72
|
+
target: join(agentRoot, ".agents", "skills"),
|
|
73
|
+
staging: join(agentRoot, ".agents", `.skills-next-${id}`),
|
|
74
|
+
backup: join(agentRoot, ".agents", `.skills-previous-${id}`),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
runtime: "claude",
|
|
78
|
+
target: join(agentRoot, ".crew", "claude-skills", ".claude", "skills"),
|
|
79
|
+
staging: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-next-${id}`),
|
|
80
|
+
backup: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-previous-${id}`),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
try {
|
|
84
|
+
for (const target of targets) {
|
|
85
|
+
await mkdir(dirname(target.target), { recursive: true });
|
|
86
|
+
await mkdir(target.staging, { mode: 0o700 });
|
|
87
|
+
for (const item of linked) {
|
|
88
|
+
await symlink(item.skill.sourcePath, join(target.staging, item.binding.skillName), "dir");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const target of targets)
|
|
92
|
+
await deps.beforeSwitch?.(target.runtime);
|
|
93
|
+
await switchProjectionSet(targets);
|
|
94
|
+
await writeFile(join(agentRoot, ".nowwork-root"), "", { encoding: "utf8", mode: 0o600 });
|
|
95
|
+
return Object.freeze(resolutions);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
await Promise.all(targets.map((target) => rm(target.staging, { recursive: true, force: true })));
|
|
99
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
reconcile(handle, bindings) {
|
|
104
|
+
return deps.coordinator.runExclusive(deps.agentsRoot, handle, () => reconcileUnlocked(handle, bindings));
|
|
105
|
+
},
|
|
106
|
+
prepareAndLaunch(agentsRoot, handle, bindings, launch) {
|
|
107
|
+
if (agentsRoot !== deps.agentsRoot) {
|
|
108
|
+
return Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
109
|
+
}
|
|
110
|
+
return deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => {
|
|
111
|
+
await reconcileUnlocked(handle, bindings);
|
|
112
|
+
return launch();
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { isProjectId, MAX_MACHINE_PROJECTS } from "./types.js";
|
|
5
|
+
import { createPromiseTail } from "../promise-tail.js";
|
|
6
|
+
import { atomicPrivateWrite } from "../atomic-private-write.js";
|
|
7
|
+
const RegistryFileSchema = z.object({
|
|
8
|
+
version: z.literal(1),
|
|
9
|
+
projects: z.record(z.object({ root: z.string().min(1).max(4_096) }).strict()),
|
|
10
|
+
}).strict();
|
|
11
|
+
export class ProjectRegistryError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code) {
|
|
14
|
+
super(code);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = "ProjectRegistryError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const immutableList = (projects) => {
|
|
20
|
+
const sorted = Object.entries(projects)
|
|
21
|
+
.map(([projectId, project]) => Object.freeze({ projectId, root: project.root }))
|
|
22
|
+
.sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
23
|
+
if (sorted.length <= MAX_MACHINE_PROJECTS)
|
|
24
|
+
return Object.freeze(sorted);
|
|
25
|
+
const visible = sorted.slice(0, MAX_MACHINE_PROJECTS);
|
|
26
|
+
const boundary = visible.at(-1);
|
|
27
|
+
if (boundary !== undefined) {
|
|
28
|
+
visible[visible.length - 1] = Object.freeze({
|
|
29
|
+
...boundary,
|
|
30
|
+
errorCode: "project_registry_limit_exceeded",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze(visible);
|
|
34
|
+
};
|
|
35
|
+
export function createProjectRegistry(agentsRoot) {
|
|
36
|
+
const path = join(agentsRoot, ".crew", "projects.json");
|
|
37
|
+
const writeTail = createPromiseTail();
|
|
38
|
+
const load = async () => {
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = await readFile(path, "utf8");
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === "ENOENT")
|
|
45
|
+
return Object.freeze({});
|
|
46
|
+
throw new ProjectRegistryError("project_registry_corrupt");
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const parsed = RegistryFileSchema.parse(JSON.parse(raw));
|
|
50
|
+
for (const [projectId, project] of Object.entries(parsed.projects)) {
|
|
51
|
+
if (!isProjectId(projectId) || !isAbsolute(project.root)) {
|
|
52
|
+
throw new ProjectRegistryError("project_registry_corrupt");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Object.freeze(Object.fromEntries(Object.entries(parsed.projects).map(([projectId, project]) => [
|
|
56
|
+
projectId,
|
|
57
|
+
Object.freeze({ root: resolve(project.root) }),
|
|
58
|
+
])));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof ProjectRegistryError)
|
|
62
|
+
throw error;
|
|
63
|
+
throw new ProjectRegistryError("project_registry_corrupt");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const save = async (projects) => {
|
|
67
|
+
await atomicPrivateWrite(path, `${JSON.stringify({ version: 1, projects }, null, 2)}\n`);
|
|
68
|
+
};
|
|
69
|
+
const enqueue = (operation) => writeTail.enqueue(operation);
|
|
70
|
+
return {
|
|
71
|
+
path,
|
|
72
|
+
async list() {
|
|
73
|
+
await writeTail.wait();
|
|
74
|
+
return immutableList(await load());
|
|
75
|
+
},
|
|
76
|
+
add(projectId, root) {
|
|
77
|
+
return enqueue(async () => {
|
|
78
|
+
if (!isProjectId(projectId))
|
|
79
|
+
throw new ProjectRegistryError("project_id_invalid");
|
|
80
|
+
if (!isAbsolute(root))
|
|
81
|
+
throw new ProjectRegistryError("project_path_invalid");
|
|
82
|
+
const normalizedRoot = resolve(root);
|
|
83
|
+
const rootStat = await stat(normalizedRoot).catch(() => null);
|
|
84
|
+
if (!rootStat?.isDirectory())
|
|
85
|
+
throw new ProjectRegistryError("project_path_invalid");
|
|
86
|
+
const projects = await load();
|
|
87
|
+
const existing = projects[projectId];
|
|
88
|
+
if (existing?.root === normalizedRoot)
|
|
89
|
+
return Object.freeze({ projectId, root: normalizedRoot });
|
|
90
|
+
if (existing !== undefined)
|
|
91
|
+
throw new ProjectRegistryError("project_id_conflict");
|
|
92
|
+
if (Object.keys(projects).length >= MAX_MACHINE_PROJECTS) {
|
|
93
|
+
throw new ProjectRegistryError("project_limit_exceeded");
|
|
94
|
+
}
|
|
95
|
+
const next = Object.freeze({ ...projects, [projectId]: Object.freeze({ root: normalizedRoot }) });
|
|
96
|
+
await save(next);
|
|
97
|
+
return Object.freeze({ projectId, root: normalizedRoot });
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
remove(projectId) {
|
|
101
|
+
return enqueue(async () => {
|
|
102
|
+
if (!isProjectId(projectId))
|
|
103
|
+
throw new ProjectRegistryError("project_id_invalid");
|
|
104
|
+
const projects = await load();
|
|
105
|
+
if (projects[projectId] === undefined)
|
|
106
|
+
return false;
|
|
107
|
+
const next = Object.fromEntries(Object.entries(projects).filter(([candidate]) => candidate !== projectId));
|
|
108
|
+
await save(Object.freeze(next));
|
|
109
|
+
return true;
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isProjectSkillName, MAX_MACHINE_PROJECT_SKILLS, MAX_PROJECT_SKILL_DESCRIPTION_LENGTH, MAX_PROJECT_SKILLS_PER_PROJECT, } from "./types.js";
|
|
4
|
+
import { parseSkillFrontmatter } from "../skill-frontmatter.js";
|
|
5
|
+
const unavailable = (projectId, scannedAt, errorCode) => Object.freeze({
|
|
6
|
+
inventory: Object.freeze({
|
|
7
|
+
projectId,
|
|
8
|
+
status: "unavailable",
|
|
9
|
+
skills: Object.freeze([]),
|
|
10
|
+
invalidSkillCount: 0,
|
|
11
|
+
errorCode,
|
|
12
|
+
scannedAt,
|
|
13
|
+
}),
|
|
14
|
+
resolved: Object.freeze([]),
|
|
15
|
+
});
|
|
16
|
+
export async function scanProject(project, now = () => new Date()) {
|
|
17
|
+
const scannedAt = now().toISOString();
|
|
18
|
+
if (project.errorCode !== undefined)
|
|
19
|
+
return unavailable(project.projectId, scannedAt, project.errorCode);
|
|
20
|
+
const projectStat = await stat(project.root).catch(() => null);
|
|
21
|
+
if (!projectStat?.isDirectory())
|
|
22
|
+
return unavailable(project.projectId, scannedAt, "project_path_unavailable");
|
|
23
|
+
const skillsRoot = join(project.root, ".agents", "skills");
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === "ENOENT") {
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
inventory: Object.freeze({
|
|
32
|
+
projectId: project.projectId,
|
|
33
|
+
status: "available",
|
|
34
|
+
skills: Object.freeze([]),
|
|
35
|
+
invalidSkillCount: 0,
|
|
36
|
+
errorCode: null,
|
|
37
|
+
scannedAt,
|
|
38
|
+
}),
|
|
39
|
+
resolved: Object.freeze([]),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return unavailable(project.projectId, scannedAt, "project_skills_unreadable");
|
|
43
|
+
}
|
|
44
|
+
let invalidSkillCount = 0;
|
|
45
|
+
const candidates = [];
|
|
46
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
47
|
+
if (entry.name.startsWith(".") || !entry.isDirectory())
|
|
48
|
+
continue;
|
|
49
|
+
const sourcePath = join(skillsRoot, entry.name);
|
|
50
|
+
const markdown = await readFile(join(sourcePath, "SKILL.md"), "utf8").catch(() => null);
|
|
51
|
+
if (markdown === null) {
|
|
52
|
+
invalidSkillCount += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const frontmatter = parseSkillFrontmatter(markdown);
|
|
56
|
+
const name = frontmatter.name;
|
|
57
|
+
const description = frontmatter.description;
|
|
58
|
+
if (name === undefined || description === undefined) {
|
|
59
|
+
invalidSkillCount += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!isProjectSkillName(name) || description.length > MAX_PROJECT_SKILL_DESCRIPTION_LENGTH) {
|
|
63
|
+
invalidSkillCount += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
candidates.push(Object.freeze({ name, description, sourcePath }));
|
|
67
|
+
}
|
|
68
|
+
const counts = new Map();
|
|
69
|
+
for (const candidate of candidates)
|
|
70
|
+
counts.set(candidate.name, (counts.get(candidate.name) ?? 0) + 1);
|
|
71
|
+
const resolved = candidates.filter((candidate) => {
|
|
72
|
+
if (counts.get(candidate.name) === 1)
|
|
73
|
+
return true;
|
|
74
|
+
invalidSkillCount += 1;
|
|
75
|
+
return false;
|
|
76
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
77
|
+
const skills = resolved.map(({ name, description }) => Object.freeze({ name, description }));
|
|
78
|
+
if (skills.length > MAX_PROJECT_SKILLS_PER_PROJECT) {
|
|
79
|
+
return unavailable(project.projectId, scannedAt, "project_skill_limit_exceeded");
|
|
80
|
+
}
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
inventory: Object.freeze({
|
|
83
|
+
projectId: project.projectId,
|
|
84
|
+
status: "available",
|
|
85
|
+
skills: Object.freeze(skills),
|
|
86
|
+
invalidSkillCount,
|
|
87
|
+
errorCode: null,
|
|
88
|
+
scannedAt,
|
|
89
|
+
}),
|
|
90
|
+
resolved: Object.freeze(resolved),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function boundScannedProjects(projects, retainedProjectIds = []) {
|
|
94
|
+
const retainedOrder = new Map(retainedProjectIds.map((projectId, index) => [projectId, index]));
|
|
95
|
+
const allocationOrder = [...projects].sort((left, right) => {
|
|
96
|
+
const leftOrder = retainedOrder.get(left.inventory.projectId);
|
|
97
|
+
const rightOrder = retainedOrder.get(right.inventory.projectId);
|
|
98
|
+
if (leftOrder !== undefined && rightOrder !== undefined)
|
|
99
|
+
return leftOrder - rightOrder;
|
|
100
|
+
if (leftOrder !== undefined)
|
|
101
|
+
return -1;
|
|
102
|
+
if (rightOrder !== undefined)
|
|
103
|
+
return 1;
|
|
104
|
+
return left.inventory.projectId.localeCompare(right.inventory.projectId);
|
|
105
|
+
});
|
|
106
|
+
let skillCount = 0;
|
|
107
|
+
const bounded = new Map();
|
|
108
|
+
for (const project of allocationOrder) {
|
|
109
|
+
if (project.inventory.status !== "available") {
|
|
110
|
+
bounded.set(project.inventory.projectId, project);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const nextCount = skillCount + project.inventory.skills.length;
|
|
114
|
+
if (nextCount <= MAX_MACHINE_PROJECT_SKILLS) {
|
|
115
|
+
skillCount = nextCount;
|
|
116
|
+
bounded.set(project.inventory.projectId, project);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
bounded.set(project.inventory.projectId, unavailable(project.inventory.projectId, project.inventory.scannedAt, "machine_project_skill_limit_exceeded"));
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze(projects.map((project) => bounded.get(project.inventory.projectId) ?? project));
|
|
122
|
+
}
|
|
123
|
+
export async function scanProjects(projects, now = () => new Date(), retainedProjectIds = []) {
|
|
124
|
+
const sorted = [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
125
|
+
return boundScannedProjects(await Promise.all(sorted.map((project) => scanProject(project, now))), retainedProjectIds);
|
|
126
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const PROJECT_SKILLS_CAPABILITY = "project_skills_v1";
|
|
2
|
+
export const MAX_PROJECT_ID_LENGTH = 64;
|
|
3
|
+
export const MAX_PROJECT_SKILL_NAME_LENGTH = 128;
|
|
4
|
+
export const MAX_PROJECT_SKILL_DESCRIPTION_LENGTH = 1_000;
|
|
5
|
+
export const MAX_PROJECT_SKILLS_PER_PROJECT = 1_000;
|
|
6
|
+
export const MAX_MACHINE_PROJECTS = 100;
|
|
7
|
+
export const MAX_MACHINE_PROJECT_SKILLS = 1_000;
|
|
8
|
+
export const MAX_AGENT_PROJECT_SKILL_BINDINGS = 500;
|
|
9
|
+
const PROJECT_ID = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
10
|
+
const SKILL_NAME = /^[a-z0-9][a-z0-9._:-]*$/u;
|
|
11
|
+
export const isProjectId = (value) => value.length > 0 && value.length <= MAX_PROJECT_ID_LENGTH && PROJECT_ID.test(value);
|
|
12
|
+
export const isProjectSkillName = (value) => value.length > 0 && value.length <= MAX_PROJECT_SKILL_NAME_LENGTH && SKILL_NAME.test(value);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function createKeyedPromiseTail() {
|
|
2
|
+
const tails = new Map();
|
|
3
|
+
return {
|
|
4
|
+
enqueue(key, operation) {
|
|
5
|
+
const previous = tails.get(key) ?? Promise.resolve();
|
|
6
|
+
const current = previous.catch(() => { }).then(operation);
|
|
7
|
+
const tail = current.then(() => undefined);
|
|
8
|
+
tails.set(key, tail);
|
|
9
|
+
void tail.finally(() => {
|
|
10
|
+
if (tails.get(key) === tail)
|
|
11
|
+
tails.delete(key);
|
|
12
|
+
}).catch(() => { });
|
|
13
|
+
return current;
|
|
14
|
+
},
|
|
15
|
+
pending: (key) => tails.get(key),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function createPromiseTail() {
|
|
19
|
+
const queue = createKeyedPromiseTail();
|
|
20
|
+
const key = "singleton";
|
|
21
|
+
return {
|
|
22
|
+
enqueue: (operation) => queue.enqueue(key, operation),
|
|
23
|
+
wait: () => queue.pending(key)?.catch(() => { }) ?? Promise.resolve(),
|
|
24
|
+
};
|
|
25
|
+
}
|
package/dist/runner.js
CHANGED
|
@@ -154,6 +154,9 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
154
154
|
exitCode: local.exitCode,
|
|
155
155
|
finalText: local.finalText,
|
|
156
156
|
errorMessage: local.errorMessage,
|
|
157
|
+
...(input.scheduled.serverOwnsFailureNotice?.() === true
|
|
158
|
+
? { suppressFailureNotice: true }
|
|
159
|
+
: {}),
|
|
157
160
|
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
158
161
|
content,
|
|
159
162
|
force: true,
|
|
@@ -194,6 +197,7 @@ export async function reportScheduledStartFailure(config, input) {
|
|
|
194
197
|
exitCode: -1,
|
|
195
198
|
finalText: null,
|
|
196
199
|
errorMessage: input.errorMessage,
|
|
200
|
+
...(input.serverOwnsFailureNotice?.() === true ? { suppressFailureNotice: true } : {}),
|
|
197
201
|
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
198
202
|
content,
|
|
199
203
|
force: true,
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -28,6 +28,8 @@ export function buildClaudeArgs(input) {
|
|
|
28
28
|
if (input.sessionId) {
|
|
29
29
|
args.push(input.resume ? "--resume" : "--session-id", input.sessionId);
|
|
30
30
|
}
|
|
31
|
+
if (input.projectSkillsDirectory)
|
|
32
|
+
args.push("--add-dir", input.projectSkillsDirectory);
|
|
31
33
|
if (input.effectivePermission === undefined) {
|
|
32
34
|
if (input.dangerous)
|
|
33
35
|
args.push("--dangerously-skip-permissions");
|
|
@@ -14,6 +14,7 @@ const RunnerInputSchema = z.object({
|
|
|
14
14
|
reasoning: z.string().min(1).optional(),
|
|
15
15
|
sessionId: z.string().min(1).optional(),
|
|
16
16
|
imagePaths: z.array(z.string().min(1)).optional(),
|
|
17
|
+
projectRootMarkers: z.array(z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/u)).max(16).optional(),
|
|
17
18
|
resume: z.boolean(),
|
|
18
19
|
}).strict();
|
|
19
20
|
const RPC_TIMEOUT_MS = 30_000;
|
|
@@ -31,6 +32,14 @@ const MAX_TRANSIENT_TURN_RETRIES = 2;
|
|
|
31
32
|
const TRANSIENT_TURN_RETRY_DELAY_MS = 15_000;
|
|
32
33
|
const TRANSIENT_TURN_RETRY_BACKOFF_FACTOR = 3;
|
|
33
34
|
const PROCESS_TREE_STOP_TIMEOUT_MS = 1_000;
|
|
35
|
+
export function codexAppServerArgs(projectRootMarkers) {
|
|
36
|
+
return [
|
|
37
|
+
...(projectRootMarkers === undefined
|
|
38
|
+
? []
|
|
39
|
+
: ["-c", `project_root_markers=${JSON.stringify(projectRootMarkers)}`]),
|
|
40
|
+
"app-server", "--listen", "stdio://",
|
|
41
|
+
];
|
|
42
|
+
}
|
|
34
43
|
const TRANSIENT_TURN_ERROR_PATTERNS = [
|
|
35
44
|
/at capacity/i,
|
|
36
45
|
/overloaded/i,
|
|
@@ -345,7 +354,7 @@ async function stopChildTree(child) {
|
|
|
345
354
|
}
|
|
346
355
|
async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs) {
|
|
347
356
|
const attemptStartedAt = Date.now();
|
|
348
|
-
const child = spawn(bin,
|
|
357
|
+
const child = spawn(bin, codexAppServerArgs(input.projectRootMarkers), {
|
|
349
358
|
cwd: process.cwd(),
|
|
350
359
|
env: process.env,
|
|
351
360
|
stdio: ["pipe", "pipe", "pipe"],
|
package/dist/scheduled-report.js
CHANGED
|
@@ -18,6 +18,9 @@ export async function deliverScheduledReport(input) {
|
|
|
18
18
|
let source = "none";
|
|
19
19
|
let content = null;
|
|
20
20
|
if (input.exitCode !== 0) {
|
|
21
|
+
if (input.suppressFailureNotice === true) {
|
|
22
|
+
return { required: false, attempted: false, delivered: false, source: "none" };
|
|
23
|
+
}
|
|
21
24
|
source = "failure_notice";
|
|
22
25
|
const detail = input.errorMessage?.trim().slice(0, 500);
|
|
23
26
|
content = `Scheduled job "${title}" failed${detail ? `: ${detail}` : ` (exit ${input.exitCode})`}.`;
|