@oatelauser/crosschat 1.0.1 → 1.2.1

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.
@@ -0,0 +1,130 @@
1
+ import { appendFileSync, closeSync, fstatSync, mkdirSync, openSync, readSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import process from 'node:process';
5
+ import { findRolloutFile } from './codex/rollout-meta.js';
6
+ /**
7
+ * Sender-side send log (B14, F4): every send's final outcome is appended as
8
+ * one JSON line so a delivery whose CLI invocation already returned (tool
9
+ * timeout → background continuation) stays observable after the fact, and a
10
+ * codex delivery is confirmed against the target's rollout file — turning
11
+ * "delivered" from "the server accepted the turn" into "the message is
12
+ * verifiably in the recipient's conversation history". Metadata only; no
13
+ * message bodies are ever written here.
14
+ */
15
+ export function defaultSendLogFile() {
16
+ return join(process.env.LOCALAPPDATA ?? homedir(), 'crosschat', 'send-log.jsonl');
17
+ }
18
+ export function appendSendLog(file, entry) {
19
+ try {
20
+ mkdirSync(dirname(file), { recursive: true });
21
+ appendFileSync(file, `${JSON.stringify(entry)}\n`, 'utf8');
22
+ }
23
+ catch {
24
+ // ponytail: observability must never break the send itself.
25
+ }
26
+ }
27
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
28
+ /**
29
+ * Poll the recipient thread's rollout file until the marker (the unique
30
+ * reply-ref embedded in every envelope) shows up — the receipt. The rollout
31
+ * write can lag turn/start acceptance by a moment, hence the short retry.
32
+ * Never throws; 'unconfirmed' means "not seen within the budget", not "lost".
33
+ */
34
+ export async function confirmInRollout(codexHome, threadId, marker, opts = {}) {
35
+ const tries = opts.tries ?? 5;
36
+ const delayMs = opts.delayMs ?? 300;
37
+ for (let attempt = 0; attempt < tries; attempt++) {
38
+ const file = findRolloutFile(codexHome, threadId);
39
+ if (file !== undefined && fileContains(file, marker))
40
+ return 'confirmed';
41
+ await sleep(delayMs);
42
+ }
43
+ return 'unconfirmed';
44
+ }
45
+ /** Tail window the conversation summaries (status batch B0) ever parse; never a full scan. */
46
+ export const SEND_LOG_TAIL_LINES = 200;
47
+ /**
48
+ * Last ~maxLines entries, read backwards from the end (stat the size, then
49
+ * 64KB chunks like fileContains below) so a large log costs a bounded read.
50
+ * The window's first line is dropped as a fragment when the cut landed
51
+ * mid-line; unparsable lines are skipped — the log is best-effort anyway.
52
+ * Missing file → empty; never throws.
53
+ */
54
+ export function readSendLogTail(file, maxLines = SEND_LOG_TAIL_LINES) {
55
+ let fd;
56
+ try {
57
+ fd = openSync(file, 'r');
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ try {
63
+ const size = fstatSync(fd).size;
64
+ const parts = [];
65
+ let pos = size;
66
+ let newlines = 0;
67
+ while (pos > 0 && newlines <= maxLines) {
68
+ const len = Math.min(64 * 1024, pos);
69
+ pos -= len;
70
+ const buf = Buffer.alloc(len);
71
+ readSync(fd, buf, 0, len, pos);
72
+ parts.unshift(buf);
73
+ // 0x0A never appears inside a multi-byte UTF-8 sequence: byte count = line count.
74
+ for (const byte of buf)
75
+ if (byte === 0x0a)
76
+ newlines++;
77
+ }
78
+ let lines = Buffer.concat(parts).toString('utf8').split('\n');
79
+ if (pos > 0)
80
+ lines = lines.slice(1); // window cut the first line mid-line
81
+ const entries = [];
82
+ for (const line of lines) {
83
+ if (line.trim() === '')
84
+ continue;
85
+ try {
86
+ entries.push(JSON.parse(line));
87
+ }
88
+ catch {
89
+ // corrupt line: skip
90
+ }
91
+ }
92
+ return entries.slice(-maxLines);
93
+ }
94
+ catch {
95
+ return [];
96
+ }
97
+ finally {
98
+ closeSync(fd);
99
+ }
100
+ }
101
+ /** Chunked substring scan; carries a tail so a marker spanning chunks is found. */
102
+ function fileContains(path, marker) {
103
+ let fd;
104
+ try {
105
+ fd = openSync(path, 'r');
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ try {
111
+ const buf = Buffer.alloc(64 * 1024);
112
+ let carry = '';
113
+ for (;;) {
114
+ const n = readSync(fd, buf, 0, buf.length, null);
115
+ if (n <= 0)
116
+ return false;
117
+ const text = carry + buf.subarray(0, n).toString('utf8');
118
+ if (text.includes(marker))
119
+ return true;
120
+ carry = text.slice(Math.max(0, text.length - marker.length));
121
+ }
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ finally {
127
+ closeSync(fd);
128
+ }
129
+ }
130
+ //# sourceMappingURL=send-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send-log.js","sourceRoot":"","sources":["../src/send-log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import process from 'node:process';
5
+ import { drain, outboxSummary } from './outbox.js';
6
+ /**
7
+ * Detached outbox watchdog (B12): the 2026-10-02 incident showed parked
8
+ * messages starve unless some CLI call happens to run while the target thread
9
+ * is idle. After any park, send spawns `crosschat __drain --watch` detached;
10
+ * it retries with backoff until the outbox empties, then exits. Single
11
+ * instance via a pid+timestamp lockfile in the outbox dir (no resident
12
+ * daemon: the process only lives while work is pending).
13
+ */
14
+ export const WATCHDOG_BACKOFF_STEPS_MS = [30_000, 60_000, 120_000, 300_000];
15
+ /** Backoff after N consecutive empty-handed rounds; any delivery resets it. */
16
+ export function nextBackoffMs(consecutiveEmptyRounds) {
17
+ const index = Math.min(Math.max(consecutiveEmptyRounds, 0), WATCHDOG_BACKOFF_STEPS_MS.length - 1);
18
+ return WATCHDOG_BACKOFF_STEPS_MS[index];
19
+ }
20
+ const WATCHDOG_LOCK = 'watchdog.lock';
21
+ /** Heartbeat freshness; a holder without a heartbeat is considered dead. */
22
+ const WATCHDOG_STALE_MS = 90_000;
23
+ export function watchdogStatus(outboxDir, nowMs = Date.now()) {
24
+ try {
25
+ const [pidText, tsText] = readFileSync(join(outboxDir, WATCHDOG_LOCK), 'utf8').trim().split(/\s+/);
26
+ const pid = Number(pidText);
27
+ const ts = Number(tsText);
28
+ if (!Number.isFinite(ts))
29
+ return { running: false };
30
+ if (nowMs - ts < WATCHDOG_STALE_MS)
31
+ return { running: true, pid: Number.isFinite(pid) ? pid : undefined };
32
+ if (Number.isFinite(pid) && pid > 0) {
33
+ try {
34
+ process.kill(pid, 0); // throws when the pid is gone
35
+ return { running: true, pid };
36
+ }
37
+ catch {
38
+ /* stale and dead */
39
+ }
40
+ }
41
+ }
42
+ catch {
43
+ /* no lock file */
44
+ }
45
+ return { running: false };
46
+ }
47
+ /**
48
+ * Best-effort spawn of the detached watchdog loop; never throws and never
49
+ * spawns a second instance while one is alive. No-op under vitest so tests
50
+ * cannot leak background processes.
51
+ */
52
+ export function spawnWatchdog(outboxDir) {
53
+ if (watchdogStatus(outboxDir).running)
54
+ return;
55
+ const entry = process.argv[1];
56
+ if (entry === undefined || entry.includes('vitest'))
57
+ return;
58
+ try {
59
+ const child = spawn(process.execPath, [entry, '__drain', '--watch'], {
60
+ detached: true,
61
+ stdio: 'ignore',
62
+ windowsHide: true,
63
+ });
64
+ child.unref();
65
+ }
66
+ catch {
67
+ // ponytail: watchdog is liveness relief, not correctness; the next send retries the spawn.
68
+ }
69
+ }
70
+ /** The watchdog loop itself; returns when the outbox is empty. Never throws. */
71
+ export async function runWatchdog(outboxDir, rateDir, deps) {
72
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
73
+ try {
74
+ mkdirSync(outboxDir, { recursive: true });
75
+ writeFileSync(join(outboxDir, WATCHDOG_LOCK), `${process.pid} ${Date.now()}`, 'utf8');
76
+ let emptyRounds = 0;
77
+ for (;;) {
78
+ const results = await drain(outboxDir, (threadId, content, busyTimeoutMs) => deps.deliverCodex(threadId, content, busyTimeoutMs), { rateDir });
79
+ const delivered = results.reduce((total, result) => total + result.delivered, 0);
80
+ const dropped = results.reduce((total, result) => total + result.dropped, 0);
81
+ if (delivered > 0)
82
+ deps.err(`watchdog: 补投 ${delivered} 条`);
83
+ if (dropped > 0)
84
+ deps.err(`watchdog: 线程已不存在,丢弃 ${dropped} 条`);
85
+ if (outboxSummary(outboxDir).every((thread) => thread.count === 0))
86
+ break;
87
+ writeFileSync(join(outboxDir, WATCHDOG_LOCK), `${process.pid} ${Date.now()}`, 'utf8'); // heartbeat
88
+ await sleep(nextBackoffMs(emptyRounds));
89
+ emptyRounds = delivered > 0 ? 0 : emptyRounds + 1;
90
+ }
91
+ }
92
+ catch {
93
+ // Never throw out of the detached loop; the next park re-spawns a fresh watchdog.
94
+ }
95
+ finally {
96
+ try {
97
+ rmSync(join(outboxDir, WATCHDOG_LOCK), { force: true });
98
+ }
99
+ catch {
100
+ /* already gone */
101
+ }
102
+ }
103
+ }
104
+ //# sourceMappingURL=watchdog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oatelauser/crosschat",
3
- "version": "1.0.1",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "Let Claude Code and Codex CLI talk to each other on your machine: native delivery via named pipe and app-server, stateless CLI, no daemon.",
6
6
  "repository": {
@@ -9,8 +9,7 @@
9
9
  },
10
10
  "license": "MIT",
11
11
  "bin": {
12
- "crosschat": "dist/cli.js",
13
- "multichat": "dist/cli.js"
12
+ "crosschat": "dist/cli.js"
14
13
  },
15
14
  "engines": {
16
15
  "node": ">=22"
@@ -28,6 +28,7 @@ crosschat send --conversation mc1_XXXX --body "正文"
28
28
  ```
29
29
 
30
30
  - `mc1_XXXX` 引用自包含路由信息,**原样照抄**,不要截断或改写。
31
+ - 对话已推进(你手里的 ref 可能是旧的)或拿不准 ref 新旧时:直接 `crosschat send --to <名字> --body "正文"`——`--to` 自动接续该端对最新对话,不必抄旧信封里的 ref。
31
32
  - `turn="N"` 是当前轮次;轮次接近你的题词/上下文预算时,主动**总结结论并收尾**,不要无限往返。
32
33
  - (曾用名 `multichat`:旧会话的信封里命令与标签可能仍写作 `multichat`/`<multichat-reply-hint>`,照抄执行即可,本机 `multichat` 命令仍然可用。)
33
34
 
@@ -41,14 +42,15 @@ crosschat send --to <名字> --body "正文"
41
42
  ```
42
43
 
43
44
  - 名字 = claude 会话名或 codex 线程名(status 列出全部可路由对象)。
45
+ - 查会话往来(双方、最近活跃、轮次、最近投递状态、滞留条数):`crosschat status --conversations`;`queued` / `parked` 的终态也在这里确认。
44
46
  - 长正文可省略 `--body`,改为管道:`<正文文件路径的内容> | crosschat send --to <名字>`。
45
- - 投给 codex 的消息:**开窗、关窗均可投**(daemon 0.160+ 且 TUI 附着 daemon 时开窗直接送达;headless turn 在后台执行,TUI 窗口不实时刷新)。若开窗被锁(旧 daemon ≤0.157 或 `--no-daemon`/未附着 TUI),crosschat 侧等待(默认至多 120s),关窗即自动送达。输出为 `delivered`(已送达)或 `parked`(对方忙,已暂存待自动补投),两者都**无需重发**。
47
+ - 投给 codex 的消息:**开窗、关窗、忙时均可投**(daemon 0.160+)。对方正在跑 turn 也不阻塞——消息直接**入队**(输出 `queued`),当前轮结束即进入对话被处理;TUI 窗口会实时刷出。输出 `delivered`(已送达)/ `queued`(已入队,轮末处理)/ `parked`(仅旧 daemon 异常态,已暂存),三者都**无需重发**。
46
48
 
47
49
  ## 报错自纠(读错误码,不要即兴发明命令)
48
50
 
49
51
  - `MESSAGE_TOO_LARGE`:正文超过 16KiB。把完整内容**写入一个文件,只发文件路径**(接收方会按需读取,这也是长内容的标准做法)。
50
52
  - `RATE_LIMITED`:发送过快(每对端点 60 秒最多 30 条)。**等待后重试**,或直接总结收尾;系统不会静默丢弃或自动重试。
51
- - `CODEX_THREAD_LOCKED` / `CODEX_THREAD_BUSY_TIMEOUT`:对方 codex 线程被窗口占用或长 turn 在跑,等待超时。消息**已暂存(parked,exit 0),无需重发**——对方空闲后任意一次 crosschat 调用(send/status)会自动补投(每线程最多暂存 20 条)。想立即送达:关闭对方 codex 窗口后再跑任意 crosschat 命令即触发补投;或改投其它信箱线程。旧 daemon(≤0.157)或 `--no-daemon`/未附着 TUI 更易触发;0.160+ 且附着时开窗可直接投。(脚注:crosschat 主通道已可开窗投递,无需 codex 的 queue 特性;0.160 实测 queue 对附着 TUI 也能秒级消费,仅作人工兜底。)
53
+ - `CODEX_THREAD_LOCKED` / `CODEX_THREAD_BUSY_TIMEOUT`:旧 daemon(≤0.157 / 未附着 TUI)下线程被窗口占用或长 turn 在跑。消息**已暂存(parked,exit 0),无需重发**——**看门狗每 0.5–5 分钟自动重投,不需要手动跑 status**;每线程最多暂存 200 条,滞留内容随时可读 `%LOCALAPPDATA%\crosschat\mailbox\<线程ID>.md`。0.160+ 正常不会再遇到:忙时直接入队(`queued`)。
52
54
  - `CALLER_IDENTITY_CONFLICT`:环境里同时残留 `CLAUDE_CODE_*` 与 `CODEX_*` 身份变量(常见于从 Claude 终端启动的 codex daemon 派生的 shell)。临时自纠 = 给命令加前缀,照抄:
53
55
 
54
56
  ```