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.
Files changed (65) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  4. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  5. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  6. package/dist/tui/linux-x64/ufoo-tui +0 -0
  7. package/package.json +11 -3
  8. package/scripts/pack-tui.js +112 -0
  9. package/scripts/postinstall.js +11 -0
  10. package/src/agents/activity/activityReconcile.js +106 -0
  11. package/src/agents/activity/activityStatePublisher.js +31 -2
  12. package/src/agents/activity/index.js +1 -0
  13. package/src/agents/launch/launcher.js +19 -0
  14. package/src/agents/launch/ptyRunner.js +20 -1
  15. package/src/app/chat/ChatController.js +433 -0
  16. package/src/app/chat/agentDirectory.js +63 -0
  17. package/src/app/chat/agentEnter.js +70 -0
  18. package/src/app/chat/agentIdentity.js +50 -0
  19. package/src/app/chat/bootstrap.js +66 -0
  20. package/src/app/chat/commandExecutor.js +108 -0
  21. package/src/app/chat/commands.js +31 -0
  22. package/src/app/chat/dashboardView.js +6 -2
  23. package/src/app/chat/historyStore.js +181 -0
  24. package/src/app/chat/index.js +14 -2
  25. package/src/app/chat/inputSubmitHandler.js +21 -7
  26. package/src/app/chat/ipcBuilders.js +52 -0
  27. package/src/app/chat/multiWindow/paneManager.js +10 -1
  28. package/src/app/chat/multiWindow/renderer.js +1 -1
  29. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  30. package/src/app/chat/streamState.js +182 -0
  31. package/src/app/cli/features/doctor.js +22 -0
  32. package/src/code/UcodeController.js +156 -0
  33. package/src/code/context/planGraphService.js +4 -0
  34. package/src/code/repl.js +4 -3
  35. package/src/code/runtime/taskLoop.js +46 -50
  36. package/src/code/tui.js +13 -2
  37. package/src/code/ucodeSlashDispatch.js +241 -0
  38. package/src/coordination/bus/activate.js +3 -0
  39. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  40. package/src/runtime/contracts/uiProtocol.js +190 -0
  41. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  42. package/src/ui/dashboardBridge.js +81 -0
  43. package/src/ui/format/index.js +2 -2
  44. package/src/ui/index.js +8 -4
  45. package/src/ui/multiPaneBusMirror.js +137 -0
  46. package/src/ui/multiWindowHandoff.js +232 -0
  47. package/src/ui/ptyHandoff.js +23 -0
  48. package/src/ui/rustChatHost.js +1520 -0
  49. package/src/ui/rustMultiSession.js +497 -0
  50. package/src/ui/rustUcodeHost.js +999 -0
  51. package/src/ui/scrollbackReplay.js +82 -0
  52. package/src/ui/settingsBridge.js +49 -0
  53. package/src/ui/toolMergeBridge.js +66 -0
  54. package/src/ui/tuiLauncher.js +105 -0
  55. package/src/ui/ucodeStatusLine.js +74 -0
  56. package/src/ui/uiHostServer.js +339 -0
  57. package/src/ui/MIGRATION.md +0 -334
  58. package/src/ui/ink/ChatApp.js +0 -4163
  59. package/src/ui/ink/DashboardBar.js +0 -691
  60. package/src/ui/ink/InkDemo.js +0 -96
  61. package/src/ui/ink/MultilineInput.js +0 -662
  62. package/src/ui/ink/UcodeApp.js +0 -1675
  63. package/src/ui/ink/agentMirror.js +0 -730
  64. package/src/ui/ink/chatReducer.js +0 -473
  65. 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/ink
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/ Ink components and pure formatting helpers
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 has no build step.
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
- Use the standard npm flow from a clean worktree:
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 pack --dry-run
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
- `npm pack --dry-run` should be used to verify the final tarball. Publishing
361
- requires an npm account/token with permission for `u-foo`.
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/ink
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/ Ink components 和纯格式化 helper
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+,没有 build step。
340
+ 本仓库是 CommonJS,目标 Node.js 18+,并通过 `dist/tui/` 附带 Rust TTY UI
341
+ (`crates/ufoo-tui`)的平台二进制。
340
342
 
341
343
  ## 发布
342
344
 
343
- 请在干净工作区按标准 npm 流程发布:
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 pack --dry-run
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
- 发布前用 `npm pack --dry-run` 检查最终 tarball。发布需要有 `u-foo` 权限的
354
- npm 账号/token。
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.10",
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
+ }
@@ -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 this default for one transition.
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
- if (state === lastState && detail === lastDetail) return false;
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
@@ -5,4 +5,5 @@ module.exports = {
5
5
  ...require("./activityStatePublisher"),
6
6
  ...require("./activityStateWriter"),
7
7
  ...require("./activityTracker"),
8
+ ...require("./activityReconcile"),
8
9
  };
@@ -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") {