u-foo 3.0.10 → 3.0.11
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 +25 -9
- package/README.zh-CN.md +22 -9
- package/dist/tui/darwin-arm64/ufoo-tui +0 -0
- package/dist/tui/darwin-x64/ufoo-tui +0 -0
- package/dist/tui/linux-arm64/ufoo-tui +0 -0
- package/dist/tui/linux-x64/ufoo-tui +0 -0
- package/package.json +11 -3
- package/scripts/pack-tui.js +112 -0
- package/scripts/postinstall.js +11 -0
- package/src/agents/activity/activityReconcile.js +106 -0
- package/src/agents/activity/activityStatePublisher.js +31 -2
- package/src/agents/activity/index.js +1 -0
- package/src/agents/launch/launcher.js +19 -0
- package/src/agents/launch/ptyRunner.js +20 -1
- package/src/app/chat/ChatController.js +433 -0
- package/src/app/chat/agentDirectory.js +63 -0
- package/src/app/chat/agentEnter.js +70 -0
- package/src/app/chat/agentIdentity.js +50 -0
- package/src/app/chat/bootstrap.js +66 -0
- package/src/app/chat/commandExecutor.js +108 -0
- package/src/app/chat/commands.js +31 -0
- package/src/app/chat/dashboardView.js +6 -2
- package/src/app/chat/historyStore.js +181 -0
- package/src/app/chat/index.js +14 -2
- package/src/app/chat/inputSubmitHandler.js +21 -7
- package/src/app/chat/ipcBuilders.js +52 -0
- package/src/app/chat/multiWindow/paneManager.js +10 -1
- package/src/app/chat/multiWindow/renderer.js +1 -1
- package/src/app/chat/multiWindow/vtFrame.js +93 -0
- package/src/app/chat/streamState.js +182 -0
- package/src/app/cli/features/doctor.js +22 -0
- package/src/code/UcodeController.js +156 -0
- package/src/code/context/planGraphService.js +4 -0
- package/src/code/repl.js +4 -3
- package/src/code/runtime/taskLoop.js +46 -50
- package/src/code/tui.js +13 -2
- package/src/code/ucodeSlashDispatch.js +241 -0
- package/src/coordination/bus/activate.js +3 -0
- package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
- package/src/runtime/contracts/uiProtocol.js +190 -0
- package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
- package/src/ui/dashboardBridge.js +81 -0
- package/src/ui/format/index.js +2 -2
- package/src/ui/index.js +8 -4
- package/src/ui/multiPaneBusMirror.js +137 -0
- package/src/ui/multiWindowHandoff.js +232 -0
- package/src/ui/ptyHandoff.js +23 -0
- package/src/ui/rustChatHost.js +1520 -0
- package/src/ui/rustMultiSession.js +497 -0
- package/src/ui/rustUcodeHost.js +999 -0
- package/src/ui/scrollbackReplay.js +82 -0
- package/src/ui/settingsBridge.js +49 -0
- package/src/ui/toolMergeBridge.js +66 -0
- package/src/ui/tuiLauncher.js +105 -0
- package/src/ui/ucodeStatusLine.js +74 -0
- package/src/ui/uiHostServer.js +339 -0
- package/src/ui/MIGRATION.md +0 -334
- package/src/ui/ink/ChatApp.js +0 -4163
- package/src/ui/ink/DashboardBar.js +0 -691
- package/src/ui/ink/InkDemo.js +0 -96
- package/src/ui/ink/MultilineInput.js +0 -662
- package/src/ui/ink/UcodeApp.js +0 -1675
- package/src/ui/ink/agentMirror.js +0 -730
- package/src/ui/ink/chatReducer.js +0 -473
- package/src/ui/runInk.js +0 -66
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ per-project MCP server mode.
|
|
|
113
113
|
|
|
114
114
|
```text
|
|
115
115
|
ufoo / ufoo chat
|
|
116
|
-
-> src/app/chat + src/ui/
|
|
116
|
+
-> src/app/chat + src/ui/rustChatHost + crates/ufoo-tui
|
|
117
117
|
-> project daemon over .ufoo/run/ufoo.sock
|
|
118
118
|
-> runtime daemon launch/resume/recover/reports/cron/groups
|
|
119
119
|
-> orchestration router, group templates, solo roles
|
|
@@ -314,7 +314,8 @@ Global `ucode` settings:
|
|
|
314
314
|
```text
|
|
315
315
|
src/
|
|
316
316
|
app/ chat client state and CLI command entry
|
|
317
|
-
ui/
|
|
317
|
+
ui/ Rust TUI hosts + pure formatting helpers
|
|
318
|
+
crates/ufoo-tui Rust ratatui child (required for chat/ucode)
|
|
318
319
|
runtime/ daemon, projects, terminal adapters, contracts, privacy, process helpers
|
|
319
320
|
coordination/ bus, context, memory, history, reports, state, status
|
|
320
321
|
orchestration/ router/controller logic, groups, solo roles
|
|
@@ -343,23 +344,38 @@ npm run test:watch
|
|
|
343
344
|
npm run test:coverage
|
|
344
345
|
```
|
|
345
346
|
|
|
346
|
-
The repository is CommonJS, targets Node.js 18+, and
|
|
347
|
+
The repository is CommonJS, targets Node.js 18+, and ships a Rust TTY UI
|
|
348
|
+
(`crates/ufoo-tui`) as platform binaries under `dist/tui/`.
|
|
347
349
|
|
|
348
350
|
## Release
|
|
349
351
|
|
|
350
|
-
|
|
352
|
+
Releases are published by GitHub Actions (`.github/workflows/release.yml`).
|
|
353
|
+
The workflow builds `ufoo-tui` for `darwin-arm64`, `darwin-x64`, `linux-x64`,
|
|
354
|
+
and `linux-arm64`, stages them under `dist/tui/<platform>/`, then runs
|
|
355
|
+
`npm publish`.
|
|
356
|
+
|
|
357
|
+
1. Bump the version and push a matching tag:
|
|
351
358
|
|
|
352
359
|
```bash
|
|
353
360
|
npm test
|
|
354
|
-
npm
|
|
355
|
-
npm version patch
|
|
356
|
-
npm publish --access public
|
|
361
|
+
npm version patch # commits package.json + creates tag vX.Y.Z
|
|
357
362
|
git push --follow-tags
|
|
358
363
|
```
|
|
359
364
|
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
2. Ensure the repo secret `NPM_TOKEN` is set (npm automation token with
|
|
366
|
+
publish rights for `u-foo`).
|
|
367
|
+
3. The `Release` workflow runs on the `v*` tag. Use **Actions → Release →
|
|
368
|
+
Run workflow** for a manual dry-run (`dry_run=true` packs but does not publish).
|
|
369
|
+
|
|
370
|
+
Local one-platform staging (dev only):
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
npm run pack:tui # cargo build + copy into dist/tui/$PLATFORM
|
|
374
|
+
npm pack --dry-run # requires at least one staged binary (prepack check)
|
|
375
|
+
```
|
|
362
376
|
|
|
377
|
+
Publishing without the GitHub workflow still requires an npm account/token with
|
|
378
|
+
permission for `u-foo`, plus staged `dist/tui/` binaries.
|
|
363
379
|
## Troubleshooting
|
|
364
380
|
|
|
365
381
|
Run a local entry directly if the linked binary is not on `PATH`:
|
package/README.zh-CN.md
CHANGED
|
@@ -110,7 +110,7 @@ MCP bridge 会连接 home 级 global controller daemon,并通过全局项目 r
|
|
|
110
110
|
|
|
111
111
|
```text
|
|
112
112
|
ufoo / ufoo chat
|
|
113
|
-
-> src/app/chat + src/ui/
|
|
113
|
+
-> src/app/chat + src/ui/rustChatHost + crates/ufoo-tui
|
|
114
114
|
-> project daemon over .ufoo/run/ufoo.sock
|
|
115
115
|
-> runtime daemon launch/resume/recover/reports/cron/groups
|
|
116
116
|
-> orchestration router, group templates, solo roles
|
|
@@ -306,7 +306,8 @@ ufoo ucode build
|
|
|
306
306
|
```text
|
|
307
307
|
src/
|
|
308
308
|
app/ chat client state 和 CLI command entry
|
|
309
|
-
ui/
|
|
309
|
+
ui/ Rust TUI hosts + 纯格式化 helper
|
|
310
|
+
crates/ufoo-tui Rust ratatui 子进程(chat/ucode 必需)
|
|
310
311
|
runtime/ daemon、projects、terminal adapters、contracts、privacy、process helpers
|
|
311
312
|
coordination/ bus、context、memory、history、reports、state、status
|
|
312
313
|
orchestration/ router/controller logic、groups、solo roles
|
|
@@ -336,22 +337,34 @@ npm run test:coverage
|
|
|
336
337
|
npm run bench:global-switch
|
|
337
338
|
```
|
|
338
339
|
|
|
339
|
-
本仓库是 CommonJS,目标 Node.js 18
|
|
340
|
+
本仓库是 CommonJS,目标 Node.js 18+,并通过 `dist/tui/` 附带 Rust TTY UI
|
|
341
|
+
(`crates/ufoo-tui`)的平台二进制。
|
|
340
342
|
|
|
341
343
|
## 发布
|
|
342
344
|
|
|
343
|
-
|
|
345
|
+
发布由 GitHub Actions(`.github/workflows/release.yml`)完成:多平台编译
|
|
346
|
+
`ufoo-tui`(`darwin-arm64` / `darwin-x64` / `linux-x64` / `linux-arm64`),
|
|
347
|
+
写入 `dist/tui/<platform>/`,再执行 `npm publish`。
|
|
348
|
+
|
|
349
|
+
1. 升版本并推送对应 tag:
|
|
344
350
|
|
|
345
351
|
```bash
|
|
346
352
|
npm test
|
|
347
|
-
npm
|
|
348
|
-
npm version patch
|
|
349
|
-
npm publish --access public
|
|
353
|
+
npm version patch # 提交 package.json 并创建 tag vX.Y.Z
|
|
350
354
|
git push --follow-tags
|
|
351
355
|
```
|
|
352
356
|
|
|
353
|
-
|
|
354
|
-
|
|
357
|
+
2. 仓库需配置 secret `NPM_TOKEN`(对 `u-foo` 有发布权限的 npm automation token)。
|
|
358
|
+
3. 推送 `v*` tag 后自动跑 `Release` workflow。也可在 Actions 里手动
|
|
359
|
+
**Run workflow**(`dry_run=true` 只打包不发布)。
|
|
360
|
+
|
|
361
|
+
本地单平台预演:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
npm run pack:tui
|
|
365
|
+
npm pack --dry-run
|
|
366
|
+
```
|
|
367
|
+
|
|
355
368
|
|
|
356
369
|
## 故障排查
|
|
357
370
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.11",
|
|
4
4
|
"description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://ufoo.dev",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Icyoung/ufoo.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Icyoung/ufoo/issues"
|
|
13
|
+
},
|
|
7
14
|
"author": "UFOO Team",
|
|
8
15
|
"keywords": [
|
|
9
16
|
"cli",
|
|
@@ -29,6 +36,7 @@
|
|
|
29
36
|
"templates/",
|
|
30
37
|
"online/",
|
|
31
38
|
"scripts/",
|
|
39
|
+
"dist/tui/",
|
|
32
40
|
"SKILLS/",
|
|
33
41
|
"LICENSE",
|
|
34
42
|
"README.md"
|
|
@@ -39,6 +47,8 @@
|
|
|
39
47
|
},
|
|
40
48
|
"scripts": {
|
|
41
49
|
"postinstall": "node scripts/postinstall.js",
|
|
50
|
+
"pack:tui": "node scripts/pack-tui.js",
|
|
51
|
+
"prepack": "node scripts/pack-tui.js --check",
|
|
42
52
|
"test": "jest",
|
|
43
53
|
"test:watch": "jest --watch",
|
|
44
54
|
"test:coverage": "jest --coverage"
|
|
@@ -51,9 +61,7 @@
|
|
|
51
61
|
"chalk": "^4.1.2",
|
|
52
62
|
"commander": "^13.1.0",
|
|
53
63
|
"gray-matter": "^4.0.3",
|
|
54
|
-
"ink": "^5.2.1",
|
|
55
64
|
"node-pty": "^1.1.0",
|
|
56
|
-
"react": "^18.3.1",
|
|
57
65
|
"ws": "^8.19.0"
|
|
58
66
|
},
|
|
59
67
|
"devDependencies": {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stage a release `ufoo-tui` binary under dist/tui/<platform>-<arch>/.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/pack-tui.js # build current host + stage
|
|
10
|
+
* node scripts/pack-tui.js --copy-only # stage from existing target/release
|
|
11
|
+
* node scripts/pack-tui.js --check # exit 0 only if at least one staged binary exists
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const { spawnSync } = require("child_process");
|
|
17
|
+
|
|
18
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
19
|
+
const DIST_ROOT = path.join(ROOT, "dist", "tui");
|
|
20
|
+
const BINARY_NAME = process.platform === "win32" ? "ufoo-tui.exe" : "ufoo-tui";
|
|
21
|
+
|
|
22
|
+
function hostPlatform() {
|
|
23
|
+
return `${process.platform}-${process.arch}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function listStagedPlatforms() {
|
|
27
|
+
if (!fs.existsSync(DIST_ROOT)) return [];
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const entry of fs.readdirSync(DIST_ROOT, { withFileTypes: true })) {
|
|
30
|
+
if (!entry.isDirectory()) continue;
|
|
31
|
+
const bin = path.join(DIST_ROOT, entry.name, "ufoo-tui");
|
|
32
|
+
const binExe = path.join(DIST_ROOT, entry.name, "ufoo-tui.exe");
|
|
33
|
+
if (fs.existsSync(bin) || fs.existsSync(binExe)) out.push(entry.name);
|
|
34
|
+
}
|
|
35
|
+
return out.sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findReleaseBinary() {
|
|
39
|
+
const candidates = [
|
|
40
|
+
path.join(ROOT, "target", "release", BINARY_NAME),
|
|
41
|
+
path.join(ROOT, "crates", "ufoo-tui", "target", "release", BINARY_NAME),
|
|
42
|
+
];
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildRelease() {
|
|
52
|
+
console.log("[pack-tui] cargo build -p ufoo-tui --release");
|
|
53
|
+
const result = spawnSync("cargo", ["build", "-p", "ufoo-tui", "--release"], {
|
|
54
|
+
cwd: ROOT,
|
|
55
|
+
stdio: "inherit",
|
|
56
|
+
env: process.env,
|
|
57
|
+
});
|
|
58
|
+
if (result.status !== 0) {
|
|
59
|
+
throw new Error("cargo build failed");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stageBinary(sourcePath, platform = hostPlatform()) {
|
|
64
|
+
const destDir = path.join(DIST_ROOT, platform);
|
|
65
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
66
|
+
const destName = platform.startsWith("win32") ? "ufoo-tui.exe" : "ufoo-tui";
|
|
67
|
+
const destPath = path.join(destDir, destName);
|
|
68
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
69
|
+
try {
|
|
70
|
+
fs.chmodSync(destPath, 0o755);
|
|
71
|
+
} catch {
|
|
72
|
+
// Windows may ignore chmod
|
|
73
|
+
}
|
|
74
|
+
console.log(`[pack-tui] staged ${destPath}`);
|
|
75
|
+
return destPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main(argv = process.argv.slice(2)) {
|
|
79
|
+
const copyOnly = argv.includes("--copy-only");
|
|
80
|
+
const checkOnly = argv.includes("--check");
|
|
81
|
+
|
|
82
|
+
if (checkOnly) {
|
|
83
|
+
const plats = listStagedPlatforms();
|
|
84
|
+
if (plats.length === 0) {
|
|
85
|
+
console.error("[pack-tui] no staged binaries under dist/tui/<platform>/");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
console.log(`[pack-tui] staged platforms: ${plats.join(", ")}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!copyOnly) {
|
|
93
|
+
buildRelease();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const source = findReleaseBinary();
|
|
97
|
+
if (!source) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`release binary not found (looked for target/release/${BINARY_NAME}). `
|
|
100
|
+
+ "Run cargo build -p ufoo-tui --release first.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
stageBinary(source);
|
|
104
|
+
console.log(`[pack-tui] ok (${hostPlatform()})`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
main();
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`[pack-tui] ${err && err.message ? err.message : err}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -30,6 +30,17 @@ for (const platform of platforms) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Ensure the platform ufoo-tui binary from the npm pack is executable.
|
|
34
|
+
try {
|
|
35
|
+
const plat = `${process.platform}-${process.arch}`;
|
|
36
|
+
const tuiBin = path.join(__dirname, "..", "dist", "tui", plat, "ufoo-tui");
|
|
37
|
+
if (fs.existsSync(tuiBin)) {
|
|
38
|
+
fs.chmodSync(tuiBin, 0o755);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Non-fatal — doctor / launch will report missing binary.
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
// Collect all skill sources from the package-level SKILLS directory.
|
|
34
45
|
function collectSkillSources(pkgRoot) {
|
|
35
46
|
const sources = [];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { readJSON } = require("../../coordination/bus/utils");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_GRACE_MS = 5000;
|
|
7
|
+
const BUSY_DISK_STATES = new Set(["working", "waiting_input", "blocked"]);
|
|
8
|
+
const DELIVERABLE_LOCAL_STATES = new Set(["idle", "ready"]);
|
|
9
|
+
|
|
10
|
+
function asState(value = "") {
|
|
11
|
+
return String(value || "").trim().toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveGraceMs(detector, options = {}) {
|
|
15
|
+
if (Number.isFinite(options.graceMs) && options.graceMs > 0) {
|
|
16
|
+
return options.graceMs;
|
|
17
|
+
}
|
|
18
|
+
const quiet = Number(detector && detector.quietWindowMs);
|
|
19
|
+
const base = Number.isFinite(quiet) && quiet > 0 ? quiet : DEFAULT_GRACE_MS;
|
|
20
|
+
// Cap so internal-pty (30s quiet) does not leave inject stamps stuck that long.
|
|
21
|
+
return Math.min(base, DEFAULT_GRACE_MS);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readDiskMeta(agentsFile, subscriber) {
|
|
25
|
+
if (!agentsFile || !subscriber || !fs.existsSync(agentsFile)) return null;
|
|
26
|
+
try {
|
|
27
|
+
const data = readJSON(agentsFile, null);
|
|
28
|
+
const meta = data && data.agents && data.agents[subscriber];
|
|
29
|
+
return meta && typeof meta === "object" ? meta : null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Heal false `working` when the in-process ActivityDetector already believes
|
|
37
|
+
* the agent is deliverable (idle/ready) but all-agents.json still says busy.
|
|
38
|
+
*
|
|
39
|
+
* Typical cause: DeliveryScheduler stamps `working` (detail=inject) after
|
|
40
|
+
* inject while the detector never saw meaningful PTY output, so it never
|
|
41
|
+
* re-publishes idle (publisher dedupe + no state transition).
|
|
42
|
+
*
|
|
43
|
+
* Waits `graceMs` (≤ quiet window, capped at 5s) after disk `activity_since`
|
|
44
|
+
* so the inject stamp can still close the mid-turn double-inject race.
|
|
45
|
+
*
|
|
46
|
+
* Does not clear `waiting_input` / `blocked` — those need explicit recovery.
|
|
47
|
+
*
|
|
48
|
+
* @returns {{ ok: boolean, reconciled: boolean, reason: string }}
|
|
49
|
+
*/
|
|
50
|
+
function reconcileDetectorOwnedActivity(options = {}) {
|
|
51
|
+
const {
|
|
52
|
+
detector,
|
|
53
|
+
publisher,
|
|
54
|
+
agentsFile,
|
|
55
|
+
subscriber,
|
|
56
|
+
now = Date.now(),
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
if (!detector || !publisher || !agentsFile || !subscriber) {
|
|
60
|
+
return { ok: false, reconciled: false, reason: "missing" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const snap = typeof detector.getState === "function"
|
|
64
|
+
? detector.getState()
|
|
65
|
+
: { state: "", since: now, detail: "" };
|
|
66
|
+
const local = asState(snap.state);
|
|
67
|
+
if (!DELIVERABLE_LOCAL_STATES.has(local)) {
|
|
68
|
+
return { ok: true, reconciled: false, reason: "local_busy" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const diskMeta = readDiskMeta(agentsFile, subscriber);
|
|
72
|
+
if (!diskMeta) {
|
|
73
|
+
return { ok: false, reconciled: false, reason: "missing_agent" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const diskState = asState(diskMeta.activity_state);
|
|
77
|
+
if (diskState !== "working") {
|
|
78
|
+
return { ok: true, reconciled: false, reason: BUSY_DISK_STATES.has(diskState) ? "disk_gated" : "disk_ok" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const graceMs = resolveGraceMs(detector, options);
|
|
82
|
+
const sinceMs = Date.parse(String(diskMeta.activity_since || ""));
|
|
83
|
+
const diskAge = Number.isFinite(sinceMs) ? Math.max(0, now - sinceMs) : graceMs;
|
|
84
|
+
if (diskAge < graceMs) {
|
|
85
|
+
return { ok: true, reconciled: false, reason: "grace" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const publishState = local === "ready" ? "idle" : local;
|
|
89
|
+
const changed = publisher.publish(publishState, {
|
|
90
|
+
since: snap.since,
|
|
91
|
+
previous: diskState,
|
|
92
|
+
detail: "",
|
|
93
|
+
}, { force: true, reconcile: true });
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
reconciled: Boolean(changed),
|
|
98
|
+
reason: changed ? "cleared" : "unchanged",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
DEFAULT_GRACE_MS,
|
|
104
|
+
reconcileDetectorOwnedActivity,
|
|
105
|
+
resolveGraceMs,
|
|
106
|
+
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
const fs = require("fs");
|
|
2
4
|
const path = require("path");
|
|
5
|
+
const { readJSON } = require("../../coordination/bus/utils");
|
|
3
6
|
const { writeActivityState } = require("./activityStateWriter");
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -16,7 +19,9 @@ const { writeActivityState } = require("./activityStateWriter");
|
|
|
16
19
|
* @param {string} options.subscriber - Subscriber ID (e.g. "claude-code:abc123")
|
|
17
20
|
* @param {string} options.projectRoot - Project root (unused, kept for API compat)
|
|
18
21
|
* @param {boolean} [options.force=true] - Force overwrite priority-protected states
|
|
19
|
-
* publish(state, extra, { force }) can override
|
|
22
|
+
* publish(state, extra, { force, reconcile }) can override defaults for one
|
|
23
|
+
* transition. `reconcile: true` skips in-memory dedupe when disk disagrees
|
|
24
|
+
* (daemon inject stamp / lost-update healing).
|
|
20
25
|
*/
|
|
21
26
|
function createActivityStatePublisher(options = {}) {
|
|
22
27
|
const {
|
|
@@ -28,9 +33,33 @@ function createActivityStatePublisher(options = {}) {
|
|
|
28
33
|
let lastState = "";
|
|
29
34
|
let lastDetail = "";
|
|
30
35
|
|
|
36
|
+
function readDiskActivity() {
|
|
37
|
+
try {
|
|
38
|
+
if (!agentsFile || !fs.existsSync(agentsFile)) return null;
|
|
39
|
+
const data = readJSON(agentsFile, null);
|
|
40
|
+
const meta = data && data.agents && data.agents[subscriber];
|
|
41
|
+
if (!meta || typeof meta !== "object") return null;
|
|
42
|
+
return {
|
|
43
|
+
state: typeof meta.activity_state === "string" ? meta.activity_state : "",
|
|
44
|
+
detail: typeof meta.activity_detail === "string" ? meta.activity_detail : "",
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
function publish(state, extra = {}, publishOptions = {}) {
|
|
32
52
|
const detail = typeof extra.detail === "string" ? extra.detail : "";
|
|
33
|
-
|
|
53
|
+
const reconcile = publishOptions.reconcile === true;
|
|
54
|
+
if (!reconcile && state === lastState && detail === lastDetail) return false;
|
|
55
|
+
if (reconcile) {
|
|
56
|
+
const disk = readDiskActivity();
|
|
57
|
+
if (disk && disk.state === state && disk.detail === detail) {
|
|
58
|
+
lastState = state;
|
|
59
|
+
lastDetail = detail;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
34
63
|
const since = extra.since || undefined;
|
|
35
64
|
const effectiveForce = typeof publishOptions.force === "boolean"
|
|
36
65
|
? publishOptions.force
|
|
@@ -10,6 +10,7 @@ const { showBanner } = require("../../ui/format/banner");
|
|
|
10
10
|
const AgentNotifier = require("./notifier");
|
|
11
11
|
const { ActivityDetector } = require("../activity/activityDetector");
|
|
12
12
|
const { createActivityStatePublisher } = require("../activity/activityStatePublisher");
|
|
13
|
+
const { reconcileDetectorOwnedActivity } = require("../activity/activityReconcile");
|
|
13
14
|
const { detectLaunchEnvironment } = require("./launchEnvironment");
|
|
14
15
|
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
15
16
|
const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
|
|
@@ -697,6 +698,23 @@ class AgentLauncher {
|
|
|
697
698
|
detail: snap.detail,
|
|
698
699
|
});
|
|
699
700
|
});
|
|
701
|
+
// Heal daemon inject-stamp desync (disk working, detector idle).
|
|
702
|
+
const agentsFilePath = getUfooPaths(this.cwd).agentsFile;
|
|
703
|
+
const activityReconcileTimer = setInterval(() => {
|
|
704
|
+
try {
|
|
705
|
+
reconcileDetectorOwnedActivity({
|
|
706
|
+
detector: launcherActivityDetector,
|
|
707
|
+
publisher: launcherPublisher,
|
|
708
|
+
agentsFile: agentsFilePath,
|
|
709
|
+
subscriber: subscriberId,
|
|
710
|
+
});
|
|
711
|
+
} catch {
|
|
712
|
+
// reconcile must never break the launcher
|
|
713
|
+
}
|
|
714
|
+
}, 2000);
|
|
715
|
+
if (typeof activityReconcileTimer.unref === "function") {
|
|
716
|
+
activityReconcileTimer.unref();
|
|
717
|
+
}
|
|
700
718
|
// Tail buffer for agy: when the TUI exits, agy prints a single line
|
|
701
719
|
// `Resume: agy --conversation=<UUID> (or -c)` to stdout. We keep
|
|
702
720
|
// the trailing ~4KB of post-frame output around and grep it on exit
|
|
@@ -763,6 +781,7 @@ class AgentLauncher {
|
|
|
763
781
|
wrapper.onExit = async ({ exitCode, signal }) => {
|
|
764
782
|
// 清理 timers
|
|
765
783
|
clearTimeout(forceReadyTimer);
|
|
784
|
+
clearInterval(activityReconcileTimer);
|
|
766
785
|
launcherActivityDetector.destroy();
|
|
767
786
|
|
|
768
787
|
// Agy: harvest conversation id from the trailing `Resume: agy
|
|
@@ -7,6 +7,7 @@ const { PTY_SOCKET_MESSAGE_TYPES, PTY_SOCKET_SUBSCRIBE_MODES } = require("../../
|
|
|
7
7
|
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
8
8
|
const { ActivityDetector } = require("../activity/activityDetector");
|
|
9
9
|
const { createActivityStatePublisher } = require("../activity/activityStatePublisher");
|
|
10
|
+
const { reconcileDetectorOwnedActivity } = require("../activity/activityReconcile");
|
|
10
11
|
const {
|
|
11
12
|
parseStreamEnvelope,
|
|
12
13
|
shouldAutoReplyFromPtyToPublisher,
|
|
@@ -734,7 +735,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
734
735
|
activityPublisher.publish(snap.state, {
|
|
735
736
|
since: snap.since,
|
|
736
737
|
detail: snap.detail,
|
|
737
|
-
});
|
|
738
|
+
}, { reconcile: true, force: true });
|
|
738
739
|
}
|
|
739
740
|
|
|
740
741
|
activityDetector.onChange((newState, oldState) => {
|
|
@@ -766,6 +767,23 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
766
767
|
// instead of waiting for the next 30s heartbeat tick.
|
|
767
768
|
writeActivityState();
|
|
768
769
|
|
|
770
|
+
// Heal daemon inject-stamp desync (disk working, detector idle/ready).
|
|
771
|
+
const activityReconcileTimer = setInterval(() => {
|
|
772
|
+
try {
|
|
773
|
+
reconcileDetectorOwnedActivity({
|
|
774
|
+
detector: activityDetector,
|
|
775
|
+
publisher: activityPublisher,
|
|
776
|
+
agentsFile: agentsFilePath,
|
|
777
|
+
subscriber,
|
|
778
|
+
});
|
|
779
|
+
} catch {
|
|
780
|
+
// reconcile must never break the runner
|
|
781
|
+
}
|
|
782
|
+
}, 2000);
|
|
783
|
+
if (typeof activityReconcileTimer.unref === "function") {
|
|
784
|
+
activityReconcileTimer.unref();
|
|
785
|
+
}
|
|
786
|
+
|
|
769
787
|
function attachPty(proc) {
|
|
770
788
|
proc.onData((data) => {
|
|
771
789
|
const raw = String(data || "");
|
|
@@ -932,6 +950,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
932
950
|
|
|
933
951
|
const stop = () => {
|
|
934
952
|
running = false;
|
|
953
|
+
clearInterval(activityReconcileTimer);
|
|
935
954
|
cleanupInjectServer(injectServer);
|
|
936
955
|
if (process.stdin && outerInputHandler) {
|
|
937
956
|
if (typeof process.stdin.off === "function") {
|