@nowcrew/daemon 0.6.68 → 0.6.69
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 +30 -1
- package/dist/agent-ability/resolver.js +27 -16
- package/dist/agent-ability/runtime.js +3 -0
- package/dist/agent-ability/types.js +6 -11
- package/dist/automatic-install-target.js +15 -10
- package/dist/background-job.js +50 -0
- package/dist/computer-cli.js +10 -1
- package/dist/computer-service.js +41 -1
- package/dist/config.js +44 -0
- package/dist/console-formatter.js +76 -1
- package/dist/console.js +23 -4
- package/dist/daemon-migration-controller.js +16 -18
- package/dist/execution-protocol.js +0 -1
- package/dist/execution-runner.js +60 -11
- package/dist/i18n.js +2 -0
- package/dist/local-execution-prompt.js +32 -13
- package/dist/local-executor.js +107 -55
- package/dist/local-memory/index-budget.js +74 -0
- package/dist/local-memory/pending.js +111 -0
- package/dist/local-memory/runtime.js +87 -0
- package/dist/local-memory/types.js +1 -0
- package/dist/local-memory/write.js +448 -0
- package/dist/machine-info.js +3 -4
- package/dist/main.js +83 -0
- package/dist/managed-service-lifecycle.js +47 -2
- package/dist/managed-service-startup.js +2 -0
- package/dist/profile-layout.js +10 -4
- package/dist/project-dependencies.js +139 -124
- package/dist/project-dependency-lock.js +87 -0
- package/dist/project-dependency-manifest.js +2 -12
- package/dist/project-registry-wiring.js +1 -4
- package/dist/project-skills/controller.js +2 -0
- package/dist/project-skills/runtime-launch.js +10 -7
- package/dist/prompt.js +19 -1
- package/dist/provider-env.js +3 -21
- package/dist/repository-identity.js +45 -0
- package/dist/runner.js +4 -2
- package/dist/runtimes/codex-app-server-runner.js +55 -23
- package/dist/runtimes/codex-command-safety.js +75 -0
- package/dist/runtimes/codex-contract.js +20 -5
- package/dist/runtimes/codex-deepseek-config.js +7 -17
- package/dist/runtimes/codex-turn-retry.js +69 -0
- package/dist/runtimes/codex.js +4 -3
- package/dist/serve.js +68 -3
- package/dist/storage-config.js +175 -0
- package/dist/storage-control.js +193 -0
- package/dist/storage-copy.js +628 -0
- package/dist/storage-credential-guard.js +21 -0
- package/dist/storage-doctor.js +228 -0
- package/dist/storage-identity.js +29 -0
- package/dist/storage-layout.js +143 -0
- package/dist/storage-manifest.js +855 -0
- package/dist/storage-migration-controller.js +420 -0
- package/dist/storage-migration-manifest.js +105 -0
- package/dist/storage-migration-policy.js +100 -0
- package/dist/storage-runtime-controller.js +339 -0
- package/dist/storage-runtime-readiness.js +184 -0
- package/dist/storage-runtime.js +39 -0
- package/dist/storage-safe-fs.js +302 -0
- package/dist/storage-serve-adapter.js +65 -0
- package/dist/storage-status-delivery.js +209 -0
- package/dist/storage-status-replay.js +125 -0
- package/dist/storage-target-confirmation.js +129 -0
- package/dist/storage-worker-command.js +37 -0
- package/dist/storage-worker-executor.js +430 -0
- package/dist/storage-worker-handoff.js +151 -0
- package/dist/storage-worker-operation.js +284 -0
- package/dist/storage-worker-status.js +259 -0
- package/dist/supervised-runtime.js +3 -1
- package/dist/workspace.js +34 -14
- package/package.json +1 -1
- package/dist/self-training-workspace.js +0 -15
|
@@ -249,6 +249,23 @@ 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);
|
|
252
269
|
const trainingSwitchJournalPath = (agentRoot) => join(agentRoot, ".training-switch.json");
|
|
253
270
|
const trainingRollbackRoot = (agentRoot) => join(agentRoot, ".training-rollback");
|
|
254
271
|
const trainingDiscardRoot = (agentRoot) => join(agentRoot, ".training-discard");
|
|
@@ -422,6 +439,7 @@ const activeTrainingRoot = async (agentsRoot, handle, ability) => {
|
|
|
422
439
|
&& catalog.releaseId === ability.releaseId
|
|
423
440
|
&& catalog.rootCommit === ability.rootCommit
|
|
424
441
|
&& catalog.artifactDigest === ability.artifactDigest
|
|
442
|
+
&& await projectedSkillsComplete(trainingRoot, ability.skills)
|
|
425
443
|
? trainingRoot
|
|
426
444
|
: null;
|
|
427
445
|
};
|
|
@@ -452,6 +470,7 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
452
470
|
const projectionDigest = abilitySha256(JSON.stringify({
|
|
453
471
|
artifactDigest: ability.artifactDigest,
|
|
454
472
|
workspace: ability.workspace,
|
|
473
|
+
git: verified.manifest.spec.git ?? null,
|
|
455
474
|
}));
|
|
456
475
|
let previousCatalog = null;
|
|
457
476
|
if (await exists(trainingRoot)) {
|
|
@@ -463,7 +482,8 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
463
482
|
}
|
|
464
483
|
if (previousCatalog.releaseId === ability.releaseId
|
|
465
484
|
&& previousCatalog.artifactDigest === ability.artifactDigest
|
|
466
|
-
&& previousCatalog.projectionDigest === projectionDigest
|
|
485
|
+
&& previousCatalog.projectionDigest === projectionDigest
|
|
486
|
+
&& await projectedSkillsComplete(trainingRoot, ability.skills))
|
|
467
487
|
return trainingRoot;
|
|
468
488
|
}
|
|
469
489
|
await mkdir(agentRoot, { recursive: true });
|
|
@@ -512,6 +532,12 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
512
532
|
: join(sourceRoot, asset.source);
|
|
513
533
|
await cp(source, target, { recursive: true, preserveTimestamps: true });
|
|
514
534
|
}
|
|
535
|
+
const gitManifestPath = verified.manifest.spec.git?.projectDependencies.path;
|
|
536
|
+
if (gitManifestPath !== undefined) {
|
|
537
|
+
const target = join(staging, "git", layerRelativePath(gitManifestPath, "git"));
|
|
538
|
+
await mkdir(dirname(target), { recursive: true });
|
|
539
|
+
await cp(join(sourceRoot, gitManifestPath), target, { preserveTimestamps: true });
|
|
540
|
+
}
|
|
515
541
|
const evalPaths = [
|
|
516
542
|
...(verified.manifest.spec.evals.policy === undefined ? [] : [verified.manifest.spec.evals.policy]),
|
|
517
543
|
...verified.manifest.spec.evals.cases,
|
|
@@ -534,6 +560,9 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
534
560
|
...asset,
|
|
535
561
|
projectedPath: `training/workspace/${abilityWorkspaceProjectionPath(asset.target)}`,
|
|
536
562
|
})),
|
|
563
|
+
git: gitManifestPath === undefined ? null : {
|
|
564
|
+
projectDependenciesPath: `training/git/${layerRelativePath(gitManifestPath, "git")}`,
|
|
565
|
+
},
|
|
537
566
|
evals: evalPaths.map((path) => `training/evals/${layerRelativePath(path, "evals")}`),
|
|
538
567
|
}, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
539
568
|
await readonlyTree(staging, { rootWritable: true });
|
|
@@ -7,10 +7,11 @@ import { parse } from "yaml";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
|
|
9
9
|
import { PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "../project-dependency-manifest.js";
|
|
10
|
+
import { normalizeRepositoryIdentity } from "../repository-identity.js";
|
|
10
11
|
const runFile = promisify(execFile);
|
|
11
12
|
const MAX_FILES = 5_000;
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
14
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
14
15
|
const MAX_INSTRUCTIONS_BYTES = 96 * 1024;
|
|
15
16
|
const MAX_MEMORY_BYTES = 64 * 1024;
|
|
16
17
|
const AUTHORING_ONLY_ROOTS = new Set([".claude"]);
|
|
@@ -78,20 +79,30 @@ const git = async (args, cwd) => {
|
|
|
78
79
|
return result.stdout.trim();
|
|
79
80
|
};
|
|
80
81
|
export const projectDependencyIds = (files, manifest) => {
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
82
|
+
const path = manifest.spec.git?.projectDependencies.path;
|
|
83
|
+
if (path === undefined)
|
|
83
84
|
return Object.freeze([]);
|
|
84
|
-
if (
|
|
85
|
-
throw new Error("
|
|
86
|
-
const file = files.find((candidate) => candidate.path ===
|
|
85
|
+
if (path !== PROJECT_MANIFEST_SOURCE_PATH)
|
|
86
|
+
throw new Error("ability_project_dependency_manifest_path_invalid");
|
|
87
|
+
const file = files.find((candidate) => candidate.path === path);
|
|
87
88
|
if (file === undefined)
|
|
88
89
|
throw new Error("ability_project_dependency_manifest_missing");
|
|
90
|
+
let projects;
|
|
89
91
|
try {
|
|
90
|
-
|
|
92
|
+
projects = ProjectDependencyManifestSchema.parse(parse(file.content.toString("utf8"))).projects;
|
|
91
93
|
}
|
|
92
94
|
catch {
|
|
93
95
|
throw new Error("ability_project_dependency_manifest_invalid");
|
|
94
96
|
}
|
|
97
|
+
return Object.freeze(projects.map((project) => {
|
|
98
|
+
try {
|
|
99
|
+
validateAbilityGitHost(project.repo);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new Error("ability_project_dependency_host_not_allowed");
|
|
103
|
+
}
|
|
104
|
+
return normalizeRepositoryIdentity(project.repo).repositoryKey;
|
|
105
|
+
}));
|
|
95
106
|
};
|
|
96
107
|
export const collectAbilityFiles = async (root) => {
|
|
97
108
|
const files = [];
|
|
@@ -114,11 +125,10 @@ export const collectAbilityFiles = async (root) => {
|
|
|
114
125
|
if (info.isDirectory())
|
|
115
126
|
await visit(absolute);
|
|
116
127
|
else if (info.isFile()) {
|
|
117
|
-
if (info.size >
|
|
128
|
+
if (info.size > MAX_FILE_BYTES)
|
|
118
129
|
throw new Error(`ability_file_size_exceeded:${relative(root, absolute)}`);
|
|
119
|
-
}
|
|
120
130
|
totalBytes += info.size;
|
|
121
|
-
if (totalBytes >
|
|
131
|
+
if (totalBytes > MAX_TOTAL_BYTES)
|
|
122
132
|
throw new Error("ability_total_size_exceeded");
|
|
123
133
|
if (files.length >= MAX_FILES)
|
|
124
134
|
throw new Error("ability_file_count_exceeded");
|
|
@@ -252,7 +262,7 @@ export const validateAbilityDependencyDeclarations = (manifest, lock) => {
|
|
|
252
262
|
throw new Error("ability_lock_has_undeclared_dependency");
|
|
253
263
|
}
|
|
254
264
|
};
|
|
255
|
-
|
|
265
|
+
const scan = (files, manifest) => {
|
|
256
266
|
const findings = [];
|
|
257
267
|
const secrets = [
|
|
258
268
|
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/u,
|
|
@@ -260,9 +270,7 @@ export const scanAbilityFiles = (files, manifest) => {
|
|
|
260
270
|
/\bAK(?:ID|IA)[A-Z0-9]{12,}\b/u,
|
|
261
271
|
];
|
|
262
272
|
const executable = new Set([".exe", ".dll", ".dylib", ".so", ".bin", ".class", ".jar"]);
|
|
263
|
-
const allowedBinaryDocuments = new Set([
|
|
264
|
-
".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".xlsx", ".xls", ".docx", ".pptx", ".ttf", ".otf",
|
|
265
|
-
]);
|
|
273
|
+
const allowedBinaryDocuments = new Set([".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".xlsx", ".xls", ".docx", ".pptx"]);
|
|
266
274
|
for (const file of files) {
|
|
267
275
|
let text;
|
|
268
276
|
try {
|
|
@@ -288,6 +296,8 @@ export const scanAbilityFiles = (files, manifest) => {
|
|
|
288
296
|
required.add("memory-seed");
|
|
289
297
|
if (manifest.spec.workspace.assets.length > 0)
|
|
290
298
|
required.add("workspace");
|
|
299
|
+
if (manifest.spec.git !== undefined)
|
|
300
|
+
required.add("git");
|
|
291
301
|
if (manifest.spec.evals.policy !== undefined)
|
|
292
302
|
required.add("eval-policy");
|
|
293
303
|
if (manifest.spec.evals.cases.length > 0)
|
|
@@ -300,6 +310,7 @@ const lockedManifestPaths = (manifest) => [
|
|
|
300
310
|
...manifest.spec.memory.policy.map((entry) => entry.path),
|
|
301
311
|
...manifest.spec.memory.seeds.map((entry) => entry.path),
|
|
302
312
|
...manifest.spec.workspace.assets.map((entry) => entry.source),
|
|
313
|
+
...(manifest.spec.git === undefined ? [] : [manifest.spec.git.projectDependencies.path]),
|
|
303
314
|
...(manifest.spec.evals.policy === undefined ? [] : [manifest.spec.evals.policy]),
|
|
304
315
|
...manifest.spec.evals.cases,
|
|
305
316
|
];
|
|
@@ -385,7 +396,7 @@ export async function resolveAbilityRelease(agentsRoot, command) {
|
|
|
385
396
|
}
|
|
386
397
|
const externalPaths = await verifyExternalDependencies(cacheRoot, resolved.root, lock);
|
|
387
398
|
files = await collectAbilityFiles(resolved.root);
|
|
388
|
-
const security =
|
|
399
|
+
const security = scan(files, manifest);
|
|
389
400
|
const kinds = new Set(lock.assets.map((asset) => asset.kind));
|
|
390
401
|
const memory = [];
|
|
391
402
|
for (const [layer, mounts] of [["release-policy", manifest.spec.memory.policy], ["release-seed", manifest.spec.memory.seeds]]) {
|
|
@@ -32,6 +32,9 @@ export function createAgentAbilityRuntime(agentsRoot, options = {}) {
|
|
|
32
32
|
else if (type === "agent:ability:resolve") {
|
|
33
33
|
result = await controller.handle(input);
|
|
34
34
|
}
|
|
35
|
+
else if (options.isAdmissionClosed?.() === true) {
|
|
36
|
+
result = { ok: false, error: "storage_migration_quiescing" };
|
|
37
|
+
}
|
|
35
38
|
else {
|
|
36
39
|
const parsed = AbilityApplyCommandSchema.safeParse(input);
|
|
37
40
|
if (!parsed.success)
|
|
@@ -19,7 +19,7 @@ export const AbilityRepositoryUrlSchema = z.string().trim().min(1).max(2_048).su
|
|
|
19
19
|
try {
|
|
20
20
|
const url = new URL(value);
|
|
21
21
|
allowedUrl = (url.protocol === "https:" || url.protocol === "ssh:")
|
|
22
|
-
&& !url.username && !url.password && Boolean(url.hostname);
|
|
22
|
+
&& !url.username && !url.password && !url.search && !url.hash && Boolean(url.hostname);
|
|
23
23
|
}
|
|
24
24
|
catch { /* handled below */ }
|
|
25
25
|
if (!scpLike && !allowedUrl)
|
|
@@ -53,6 +53,10 @@ const LocalSkillSchema = z.object({ type: z.literal("local"), path: SafeAbilityP
|
|
|
53
53
|
const GitSkillSchema = z.object({
|
|
54
54
|
type: z.literal("git"), url: AbilityRepositoryUrlSchema, ref: z.string().min(1).max(256), path: SafeAbilityPathSchema,
|
|
55
55
|
}).strict();
|
|
56
|
+
const GitProjectDependencyPathSchema = z.literal("git/project-dependencies.yaml");
|
|
57
|
+
const GitLayerSchema = z.object({
|
|
58
|
+
projectDependencies: z.object({ path: GitProjectDependencyPathSchema }).strict(),
|
|
59
|
+
}).strict();
|
|
56
60
|
export const DaemonAbilityManifestSchema = z.object({
|
|
57
61
|
apiVersion: z.literal("nowcrew.ai/v1alpha1"), kind: z.literal("AgentTraining"),
|
|
58
62
|
metadata: z.object({ name: z.string().min(1).max(128), version: z.string().min(1).max(64), description: z.string().max(1_000).optional() }).strict(),
|
|
@@ -71,6 +75,7 @@ export const DaemonAbilityManifestSchema = z.object({
|
|
|
71
75
|
workspace: z.object({ assets: z.array(z.object({
|
|
72
76
|
source: SafeAbilityPathSchema, target: WorkspaceAbilityTargetSchema, mode: ModeSchema,
|
|
73
77
|
}).strict()).max(1_000).default([]) }).strict().default({ assets: [] }),
|
|
78
|
+
git: GitLayerSchema.optional(),
|
|
74
79
|
evals: z.object({
|
|
75
80
|
policy: SafeAbilityPathSchema.optional(),
|
|
76
81
|
cases: z.array(SafeAbilityPathSchema).max(1_000).default([]),
|
|
@@ -95,16 +100,6 @@ export const DaemonAbilityManifestSchema = z.object({
|
|
|
95
100
|
unique(manifest.spec.skills.map((skill) => skill.name.replace(/[^A-Za-z0-9._-]/gu, "_")), ["spec", "skills"], "Skill projection");
|
|
96
101
|
unique(manifest.spec.skills.map((skill) => skill.mount), ["spec", "skills"], "Skill mount");
|
|
97
102
|
unique(manifest.spec.workspace.assets.map((asset) => asset.target), ["spec", "workspace", "assets"], "Workspace target");
|
|
98
|
-
for (const asset of manifest.spec.workspace.assets) {
|
|
99
|
-
if (asset.source === "workspace/project-dependencies.yaml"
|
|
100
|
-
&& asset.target !== "training/workspace/project-dependencies.yaml") {
|
|
101
|
-
ctx.addIssue({
|
|
102
|
-
code: z.ZodIssueCode.custom,
|
|
103
|
-
path: ["spec", "workspace", "assets"],
|
|
104
|
-
message: "project dependency manifest target must be training/workspace/project-dependencies.yaml",
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
103
|
unique(manifest.spec.evals.cases, ["spec", "evals", "cases"], "Eval case");
|
|
109
104
|
});
|
|
110
105
|
export const DaemonAbilityLockSchema = z.object({
|
|
@@ -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
|
+
environment: "production",
|
|
7
7
|
daemonDirectory: "daemon",
|
|
8
8
|
agentsDirectory: "agents",
|
|
9
9
|
},
|
|
10
10
|
"nowwork-dev.nowcoder.com": {
|
|
11
|
-
|
|
11
|
+
environment: "dev",
|
|
12
12
|
daemonDirectory: "daemon-dev",
|
|
13
13
|
agentsDirectory: "agents-dev",
|
|
14
14
|
},
|
|
@@ -42,32 +42,37 @@ 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" || value === "production") {
|
|
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
|
-
|
|
52
|
+
// Keep the no-flag target readable by old daemons; any workspace-scoped profile below
|
|
53
|
+
// uses the canonical environment token (production, never in).
|
|
54
|
+
const legacyProfile = environment === "production" ? "in" : environment;
|
|
53
55
|
if (requestedProfile === undefined || requestedProfile === legacyProfile) {
|
|
54
56
|
return Object.freeze({
|
|
55
57
|
kind: "known",
|
|
56
58
|
profile: legacyProfile,
|
|
57
59
|
serverUrl: normalizedServerUrl(parsed),
|
|
58
|
-
daemonHome: paths.join(input.userHome, ".crew", environment === "
|
|
59
|
-
agentsRoot: paths.join(input.userHome, ".crew", environment === "
|
|
60
|
+
daemonHome: paths.join(input.userHome, ".crew", environment === "production" ? "daemon" : "daemon-dev"),
|
|
61
|
+
agentsRoot: paths.join(input.userHome, ".crew", environment === "production" ? "agents" : "agents-dev"),
|
|
60
62
|
npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", legacyProfile),
|
|
61
63
|
});
|
|
62
64
|
}
|
|
63
65
|
if (requestedProfile === "in" || requestedProfile === "dev") {
|
|
64
|
-
throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${
|
|
66
|
+
throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${environment}'`);
|
|
65
67
|
}
|
|
66
68
|
const prefix = `nw-${environment}-`;
|
|
67
|
-
|
|
69
|
+
const legacyPrefix = environment === "production" ? "nw-in-" : prefix;
|
|
70
|
+
if (!requestedProfile.startsWith(prefix) && !requestedProfile.startsWith(legacyPrefix)) {
|
|
68
71
|
throw new Error(`Server hostname '${new URL(input.serverUrl).hostname}' requires profile '${prefix}<local-name>'`);
|
|
69
72
|
}
|
|
70
|
-
const localInstanceName = validateLocalInstanceName(requestedProfile.
|
|
73
|
+
const localInstanceName = validateLocalInstanceName(requestedProfile.startsWith(prefix)
|
|
74
|
+
? requestedProfile.slice(prefix.length)
|
|
75
|
+
: requestedProfile.slice(legacyPrefix.length));
|
|
71
76
|
const layout = profileLayout({
|
|
72
77
|
environment,
|
|
73
78
|
localInstanceName,
|
|
@@ -91,7 +96,7 @@ export function resolveAutomaticInstallTarget(input) {
|
|
|
91
96
|
const paths = input.platform === "win32" ? win32 : posix;
|
|
92
97
|
const known = KNOWN_TARGETS[hostname];
|
|
93
98
|
if (known !== undefined) {
|
|
94
|
-
return knownTargetForProfile(known.
|
|
99
|
+
return knownTargetForProfile(known.environment, input.requestedProfile, input, parsed);
|
|
95
100
|
}
|
|
96
101
|
const profile = requireCustomProfile(hostname, input.requestedProfile);
|
|
97
102
|
return Object.freeze({
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude 后台任务(Bash run_in_background + TaskOutput 轮询)的结果解析。纯函数,零 IO。
|
|
3
|
+
*
|
|
4
|
+
* TaskOutput 的 tool_result 是 XML 式文本,实测形状见
|
|
5
|
+
* docs/superpowers/evidence/2026-08-22-claude-background-task-stream-json.json。
|
|
6
|
+
* 注意轮询工具是 TaskOutput 而非 BashOutput——claude 2.1.238 实测确认。
|
|
7
|
+
*/
|
|
8
|
+
/** 输出正文的字符上限,与 console 其它工具返回保持同量级。 */
|
|
9
|
+
const OUTPUT_CAP = 4000;
|
|
10
|
+
const tag = (text, name) => {
|
|
11
|
+
const match = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "u").exec(text);
|
|
12
|
+
return match ? match[1].trim() : undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* <output> 是协议里的最后一段,正文本身(命令的 stdout)可能包含字面量的 "</output>"
|
|
16
|
+
* (例如打印了一份 XML/日志)。非贪婪匹配会在第一个 "</output>" 处截断,把后面的真实内容
|
|
17
|
+
* 静默丢掉,产出一个"看起来对但其实错"的结果。这里改为贪婪匹配,吞到文本中*最后一个*
|
|
18
|
+
* "</output>",与协议里 output 恒为末段的事实对齐。
|
|
19
|
+
*/
|
|
20
|
+
const extractOutputTag = (text) => {
|
|
21
|
+
const match = /<output>([\s\S]*)<\/output>/u.exec(text);
|
|
22
|
+
return match ? match[1].trim() : undefined;
|
|
23
|
+
};
|
|
24
|
+
export function parseTaskOutput(text) {
|
|
25
|
+
const jobId = tag(text, "task_id");
|
|
26
|
+
if (!jobId)
|
|
27
|
+
return null;
|
|
28
|
+
const status = tag(text, "status");
|
|
29
|
+
if (!status)
|
|
30
|
+
return null;
|
|
31
|
+
const rawExit = tag(text, "exit_code");
|
|
32
|
+
const exitCode = rawExit !== undefined && /^-?\d+$/u.test(rawExit) ? Number(rawExit) : undefined;
|
|
33
|
+
// status 只要不是 running,就按已结束处理(completed/failed/killed 等都归入 exited)。
|
|
34
|
+
const state = status === "running" ? "running" : "exited";
|
|
35
|
+
const rawOutput = extractOutputTag(text);
|
|
36
|
+
return {
|
|
37
|
+
jobId,
|
|
38
|
+
state,
|
|
39
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
40
|
+
// lineCount 必须基于截断前的完整正文——否则前端拿到的"总行数"其实是预览的行数,
|
|
41
|
+
// 无法用来判断是否还有更多内容可展开(参照 console.ts 里 buildScriptResult 的做法)。
|
|
42
|
+
...(rawOutput
|
|
43
|
+
? {
|
|
44
|
+
output: rawOutput.slice(0, OUTPUT_CAP),
|
|
45
|
+
lineCount: rawOutput.split("\n").length,
|
|
46
|
+
...(rawOutput.length > OUTPUT_CAP ? { truncated: true } : {}),
|
|
47
|
+
}
|
|
48
|
+
: {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
package/dist/computer-cli.js
CHANGED
|
@@ -82,6 +82,14 @@ function translatedError(error, td) {
|
|
|
82
82
|
}
|
|
83
83
|
return td(error.message);
|
|
84
84
|
}
|
|
85
|
+
function storageServiceEnvironment(env) {
|
|
86
|
+
return {
|
|
87
|
+
...(env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: env.NOWWORK_HOME }),
|
|
88
|
+
...(env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: env.NOWWORK_ENV_TAG }),
|
|
89
|
+
...(env.NOWWORK_STORAGE_POLICY_PATH === undefined
|
|
90
|
+
? {} : { NOWWORK_STORAGE_POLICY_PATH: env.NOWWORK_STORAGE_POLICY_PATH }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
85
93
|
export async function runComputerCommand(argv, overrides = {}) {
|
|
86
94
|
const command = argv[0];
|
|
87
95
|
if (!command || !COMPUTER_COMMANDS.has(command))
|
|
@@ -202,6 +210,7 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
202
210
|
...deps,
|
|
203
211
|
profile: restartProfile.name,
|
|
204
212
|
profileHome: home,
|
|
213
|
+
environment: storageServiceEnvironment(deps.env),
|
|
205
214
|
})
|
|
206
215
|
: null;
|
|
207
216
|
if (spec && restartProfile) {
|
|
@@ -272,7 +281,7 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
272
281
|
throw new Error(td("Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry"));
|
|
273
282
|
}
|
|
274
283
|
const spec = buildServiceSpec({
|
|
275
|
-
...deps, profile: name, profileHome: home,
|
|
284
|
+
...deps, profile: name, profileHome: home, environment: storageServiceEnvironment(deps.env),
|
|
276
285
|
});
|
|
277
286
|
const installation = daemonGlobalInstallation(deps.entryPath, deps.platform);
|
|
278
287
|
const managedRecord = installation !== null && spec.platform !== "win32"
|
package/dist/computer-service.js
CHANGED
|
@@ -53,6 +53,7 @@ export function windowsCommandArg(value) {
|
|
|
53
53
|
}
|
|
54
54
|
export function buildServiceSpec(input) {
|
|
55
55
|
const { platform, profile, userHome } = input;
|
|
56
|
+
const environment = input.environment ?? process.env;
|
|
56
57
|
if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
|
|
57
58
|
throw new Error(`Computer services are not supported on platform '${platform}'`);
|
|
58
59
|
}
|
|
@@ -72,6 +73,16 @@ export function buildServiceSpec(input) {
|
|
|
72
73
|
throw new Error("Cannot install launchd service without a numeric uid");
|
|
73
74
|
const id = `com.nowcrew.daemon.${profile}${identitySuffix}`;
|
|
74
75
|
const args = daemonCommand.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
|
|
76
|
+
const environmentEntries = [
|
|
77
|
+
["NOWWORK_HOME", environment.NOWWORK_HOME],
|
|
78
|
+
["NOWWORK_ENV_TAG", environment.NOWWORK_ENV_TAG],
|
|
79
|
+
["NOWWORK_STORAGE_POLICY_PATH", environment.NOWWORK_STORAGE_POLICY_PATH],
|
|
80
|
+
].flatMap(([key, value]) => value === undefined
|
|
81
|
+
? []
|
|
82
|
+
: [` <key>${key}</key><string>${xml(assertSingleLine(value, key))}</string>`]);
|
|
83
|
+
const environmentBlock = environmentEntries.length === 0
|
|
84
|
+
? ""
|
|
85
|
+
: ` <key>EnvironmentVariables</key>\n <dict>\n${environmentEntries.join("\n")}\n </dict>\n`;
|
|
75
86
|
return {
|
|
76
87
|
platform,
|
|
77
88
|
profile,
|
|
@@ -79,7 +90,7 @@ export function buildServiceSpec(input) {
|
|
|
79
90
|
descriptorPath: resolve(userHome, "Library", "LaunchAgents", `${id}.plist`),
|
|
80
91
|
daemonCommand,
|
|
81
92
|
managerDomain: `gui/${input.uid}`,
|
|
82
|
-
descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
|
|
93
|
+
descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n${environmentBlock} <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
|
|
83
94
|
};
|
|
84
95
|
}
|
|
85
96
|
if (platform === "linux") {
|
|
@@ -266,6 +277,35 @@ export async function installService(spec, runner = systemCommandRunner) {
|
|
|
266
277
|
throw error;
|
|
267
278
|
}
|
|
268
279
|
}
|
|
280
|
+
export async function replaceInstalledServiceDescriptor(spec, previousDescriptor, runner = systemCommandRunner) {
|
|
281
|
+
if (spec.platform === "win32" || spec.descriptorPath === null || spec.descriptor === null) {
|
|
282
|
+
throw new Error("Installed service descriptor replacement requires launchd or systemd");
|
|
283
|
+
}
|
|
284
|
+
const current = await readServiceDescriptor(spec);
|
|
285
|
+
if (current !== previousDescriptor) {
|
|
286
|
+
throw new Error(`Installed descriptor for '${spec.id}' changed before replacement`);
|
|
287
|
+
}
|
|
288
|
+
const status = await serviceStatus(spec, runner);
|
|
289
|
+
if (!status.installed)
|
|
290
|
+
throw new Error(`Service '${spec.id}' is not installed`);
|
|
291
|
+
if (status.running || status.loaded)
|
|
292
|
+
await serviceAction(spec, "stop", runner);
|
|
293
|
+
try {
|
|
294
|
+
await writePrivateAtomic(spec.descriptorPath, spec.descriptor);
|
|
295
|
+
if (spec.platform === "linux") {
|
|
296
|
+
await checked(runner, "systemctl", ["--user", "daemon-reload"], "systemd reload");
|
|
297
|
+
await checked(runner, "systemctl", ["--user", "enable", spec.id], "systemd enable");
|
|
298
|
+
}
|
|
299
|
+
if (status.running)
|
|
300
|
+
await serviceAction(spec, "start", runner);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
await writePrivateAtomic(spec.descriptorPath, previousDescriptor).catch(() => undefined);
|
|
304
|
+
if (status.running)
|
|
305
|
+
await serviceAction(spec, "start", runner).catch(() => undefined);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
269
309
|
export async function ensureServiceEnabled(spec, runner = systemCommandRunner) {
|
|
270
310
|
if (spec.platform === "linux") {
|
|
271
311
|
if (spec.descriptorPath === null || !await pathExists(spec.descriptorPath)) {
|
package/dist/config.js
CHANGED
|
@@ -59,6 +59,40 @@ 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
|
+
}
|
|
62
96
|
export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
|
|
63
97
|
function codexHomeModeEnv(env) {
|
|
64
98
|
const value = env.CREW_CODEX_HOME_MODE;
|
|
@@ -68,6 +102,14 @@ function codexHomeModeEnv(env) {
|
|
|
68
102
|
return "private";
|
|
69
103
|
throw new ConfigError("CREW_CODEX_HOME_MODE must be shared or private");
|
|
70
104
|
}
|
|
105
|
+
function codexCollaborationToolsEnv(env) {
|
|
106
|
+
const value = (env.CREW_CODEX_COLLABORATION_TOOLS ?? "").trim().toLowerCase();
|
|
107
|
+
if (value === "" || value === "0" || value === "false" || value === "off")
|
|
108
|
+
return false;
|
|
109
|
+
if (value === "1" || value === "true" || value === "on")
|
|
110
|
+
return true;
|
|
111
|
+
throw new ConfigError("CREW_CODEX_COLLABORATION_TOOLS must be one of 1/true/on/0/false/off");
|
|
112
|
+
}
|
|
71
113
|
export function loadConfig(env = process.env) {
|
|
72
114
|
const serverUrl = (env.CREW_SERVER_URL ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
|
|
73
115
|
const machineToken = env.CREW_MACHINE_TOKEN ?? "";
|
|
@@ -99,6 +141,7 @@ export function loadConfig(env = process.env) {
|
|
|
99
141
|
cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
|
|
100
142
|
runtimeBin: env.CREW_RUNTIME ?? "claude",
|
|
101
143
|
codexHomeMode: codexHomeModeEnv(env),
|
|
144
|
+
codexCollaborationTools: codexCollaborationToolsEnv(env),
|
|
102
145
|
dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
|
|
103
146
|
resume: env.CREW_RESUME !== "off" && env.CREW_RESUME !== "0", // 默认开启;一键回退现状用 CREW_RESUME=off
|
|
104
147
|
resumeWarmMs: env.CREW_RESUME_WARM_MS != null ? Number(env.CREW_RESUME_WARM_MS) : 3_600_000, // 默认 1h
|
|
@@ -108,6 +151,7 @@ export function loadConfig(env = process.env) {
|
|
|
108
151
|
sessionMaxTurns: env.CREW_SESSION_MAX_TURNS != null ? Number(env.CREW_SESSION_MAX_TURNS) : 30,
|
|
109
152
|
productName: env.CREW_PRODUCT_NAME ?? "nowwork",
|
|
110
153
|
agentMemory: loadAgentMemoryConfig(env),
|
|
154
|
+
localMemoryV2Handles: localMemoryV2Handles(env, serverUrl),
|
|
111
155
|
executionLimits,
|
|
112
156
|
};
|
|
113
157
|
}
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* 用 tool_use id 关联 Read 结果,避免将整文件作为普通 tool_result 倾泻到终端。
|
|
4
4
|
*/
|
|
5
5
|
import { buildFilePreview } from "./console-payload.js";
|
|
6
|
+
import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
|
|
6
7
|
import { buildScriptResult, toConsoleLines, TOOL_RESULT_CAP } from "./console.js";
|
|
7
8
|
import { parseUnifiedDiff } from "./unified-diff.js";
|
|
8
9
|
import { buildJsonResult } from "./json-result.js";
|
|
9
10
|
import { buildCollapsedResult } from "./console-collapse.js";
|
|
10
11
|
import { buildSkillPreview } from "./skill-preview.js";
|
|
12
|
+
import { parseTaskOutput } from "./background-job.js";
|
|
11
13
|
const extract = (content) => {
|
|
12
14
|
if (typeof content === "string")
|
|
13
15
|
return content;
|
|
@@ -22,16 +24,71 @@ const clip = (text) => text.length > TOOL_RESULT_CAP
|
|
|
22
24
|
const SCRIPT_TOOLS = new Set(["Bash", "PowerShell", "Shell"]);
|
|
23
25
|
export function createConsoleFormatter() {
|
|
24
26
|
const pending = new Map();
|
|
27
|
+
// listId → 上一条 todos 快照的指纹;仅用于丢弃「连续两次完全相同」的重复事件(无损去重)。
|
|
28
|
+
// 只比对相邻快照,不做跨快照合并——进度推进的中间态必须逐条下发,否则前端进度条会停在旧值。
|
|
29
|
+
const lastTodoFingerprint = new Map();
|
|
30
|
+
// jobId → 已下发次数(去重后);后台任务在滚动流里折叠成一个单元,次数供前端展示。
|
|
31
|
+
// 与去重表联动:被丢弃的重复轮询不会走到这里递增,所以计的是"不同状态数"而非"原始轮询数"。
|
|
32
|
+
const jobPollCount = new Map();
|
|
33
|
+
// jobId → 上一条轮询结果的指纹,用于丢弃「连续两次完全相同」的轮询(无损去重)。
|
|
34
|
+
const lastJobFingerprint = new Map();
|
|
35
|
+
/** 丢弃与该 listId 上一条完全相同的 todos 行;无 listId 的不参与去重。 */
|
|
36
|
+
const dedupeTodos = (chunks) => chunks.filter((chunk) => {
|
|
37
|
+
const payload = chunk.payload;
|
|
38
|
+
if (!payload || payload.kind !== "todos" || !payload.listId)
|
|
39
|
+
return true;
|
|
40
|
+
const fingerprint = JSON.stringify(payload.todos);
|
|
41
|
+
if (lastTodoFingerprint.get(payload.listId) === fingerprint)
|
|
42
|
+
return false;
|
|
43
|
+
lastTodoFingerprint.set(payload.listId, fingerprint);
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
// claude redacted-thinking(redact-thinking-2026-02-12):流式阶段 thinking_delta.thinking 为空串,
|
|
47
|
+
// 只携带 estimated_tokens 估算(数值会被修正,取最新值);完整 assistant 消息里 thinking 也为空。
|
|
48
|
+
// 这里累积估算值,在 assistant 消息落地时把空 thinking 块补成占位文本;
|
|
49
|
+
// 若未来 CLI 恢复明文思考,累积到的正文优先生效,占位逻辑自动失效。
|
|
50
|
+
let pendingThinking = null;
|
|
51
|
+
const consumePendingThinking = () => {
|
|
52
|
+
if (!pendingThinking)
|
|
53
|
+
return null;
|
|
54
|
+
const { text, tokens } = pendingThinking;
|
|
55
|
+
pendingThinking = null;
|
|
56
|
+
if (text.trim())
|
|
57
|
+
return text.trim();
|
|
58
|
+
const lang = detectDaemonLang();
|
|
59
|
+
return tokens > 0
|
|
60
|
+
? formatDaemonText(lang, "Thinking (~{{tokens}} tokens, content not streamed by Claude Code)", { tokens })
|
|
61
|
+
: translateDaemon(lang, "Thinking (content not streamed by Claude Code)");
|
|
62
|
+
};
|
|
25
63
|
return {
|
|
26
64
|
format(event) {
|
|
27
65
|
const rec = event && typeof event === "object" ? event : {};
|
|
28
66
|
const message = rec.message && typeof rec.message === "object" ? rec.message : undefined;
|
|
67
|
+
if (rec.type === "stream_event" && rec.event && typeof rec.event === "object") {
|
|
68
|
+
const streamEvent = rec.event;
|
|
69
|
+
if (streamEvent.type === "content_block_delta" && streamEvent.delta?.type === "thinking_delta") {
|
|
70
|
+
const prev = pendingThinking ?? { text: "", tokens: 0 };
|
|
71
|
+
pendingThinking = {
|
|
72
|
+
text: prev.text + (typeof streamEvent.delta.thinking === "string" ? streamEvent.delta.thinking : ""),
|
|
73
|
+
tokens: typeof streamEvent.delta.estimated_tokens === "number" ? streamEvent.delta.estimated_tokens : prev.tokens,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
29
78
|
if (rec.type === "assistant" && Array.isArray(message?.content)) {
|
|
30
79
|
for (const block of message.content) {
|
|
31
80
|
if (block.type === "tool_use" && block.id && block.name) {
|
|
32
81
|
pending.set(block.id, { name: block.name, ...(block.input ? { input: block.input } : {}) });
|
|
33
82
|
}
|
|
34
83
|
}
|
|
84
|
+
const hasEmptyThinking = message.content.some((block) => block.type === "thinking" && !block.thinking?.trim());
|
|
85
|
+
if (hasEmptyThinking) {
|
|
86
|
+
const filler = consumePendingThinking();
|
|
87
|
+
if (filler) {
|
|
88
|
+
const content = message.content.map((block) => block.type === "thinking" && !block.thinking?.trim() ? { ...block, thinking: filler } : block);
|
|
89
|
+
return dedupeTodos(toConsoleLines({ ...rec, message: { ...message, content } }));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
35
92
|
}
|
|
36
93
|
if (rec.type === "user" && Array.isArray(message?.content)) {
|
|
37
94
|
const out = [];
|
|
@@ -44,6 +101,24 @@ export function createConsoleFormatter() {
|
|
|
44
101
|
const tool = block.tool_use_id ? pending.get(block.tool_use_id) : undefined;
|
|
45
102
|
if (block.tool_use_id)
|
|
46
103
|
pending.delete(block.tool_use_id);
|
|
104
|
+
if (!block.is_error && tool?.name === "TaskOutput") {
|
|
105
|
+
const parsed = parseTaskOutput(text);
|
|
106
|
+
if (parsed) {
|
|
107
|
+
const fingerprint = JSON.stringify(parsed);
|
|
108
|
+
if (lastJobFingerprint.get(parsed.jobId) === fingerprint)
|
|
109
|
+
continue;
|
|
110
|
+
lastJobFingerprint.set(parsed.jobId, fingerprint);
|
|
111
|
+
const pollCount = (jobPollCount.get(parsed.jobId) ?? 0) + 1;
|
|
112
|
+
jobPollCount.set(parsed.jobId, pollCount);
|
|
113
|
+
out.push({
|
|
114
|
+
stream: "tool_result",
|
|
115
|
+
text: `⏺ Background job ${parsed.jobId} · ${parsed.state}`,
|
|
116
|
+
payload: { ...parsed, kind: "background_job", pollCount },
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// 解析不出来就走下面的通用分支,降级成纯文本行,不猜。
|
|
121
|
+
}
|
|
47
122
|
if (!block.is_error && tool?.name === "Read" && typeof tool.input?.file_path === "string") {
|
|
48
123
|
const start = typeof tool.input.offset === "number" && Number.isSafeInteger(tool.input.offset)
|
|
49
124
|
? Math.max(1, tool.input.offset) : 1;
|
|
@@ -71,7 +146,7 @@ export function createConsoleFormatter() {
|
|
|
71
146
|
}
|
|
72
147
|
return out;
|
|
73
148
|
}
|
|
74
|
-
return toConsoleLines(event);
|
|
149
|
+
return dedupeTodos(toConsoleLines(event));
|
|
75
150
|
},
|
|
76
151
|
};
|
|
77
152
|
}
|