@hua-labs/tap 0.2.4 → 0.2.6

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 CHANGED
@@ -1,194 +1,194 @@
1
- # @hua-labs/tap
2
-
3
- Zero-dependency CLI for cross-model AI agent communication setup.
4
-
5
- One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
6
-
7
- ## Quick Start
8
-
9
- > `bun` is required to run the managed tap MCP server. When installed from npm, `@hua-labs/tap` now ships its own bundled MCP server entry.
10
-
11
- ```bash
12
- # 1. Initialize comms directory and state
13
- npx @hua-labs/tap init
14
-
15
- # 2. Add runtimes
16
- npx @hua-labs/tap add claude
17
- npx @hua-labs/tap add codex
18
- npx @hua-labs/tap add gemini
19
-
20
- # 3. Check status
21
- npx @hua-labs/tap status
22
- ```
23
-
24
- Your agents can now communicate through the shared comms directory.
25
-
26
- ## Commands
27
-
28
- ### `init`
29
-
30
- Initialize the comms directory and `.tap-comms/` state.
31
-
32
- By default, the comms directory is created inside the current repo at `./tap-comms`.
33
-
34
- ```bash
35
- npx @hua-labs/tap init
36
- npx @hua-labs/tap init --comms-dir /path/to/comms
37
- npx @hua-labs/tap init --permissions safe # default: deny destructive ops
38
- npx @hua-labs/tap init --permissions full # no restrictions (use with caution)
39
- npx @hua-labs/tap init --force # re-initialize
40
- ```
41
-
42
- ### `add <runtime>`
43
-
44
- Add a runtime. Probes config, plans patches, applies, and verifies.
45
-
46
- ```bash
47
- npx @hua-labs/tap add claude
48
- npx @hua-labs/tap add codex
49
- npx @hua-labs/tap add gemini
50
- npx @hua-labs/tap add claude --force # re-install
51
- ```
52
-
53
- ### `remove <runtime>`
54
-
55
- Remove a runtime and rollback config changes.
56
-
57
- ```bash
58
- npx @hua-labs/tap remove claude
59
- npx @hua-labs/tap remove codex
60
- ```
61
-
62
- ### `status`
63
-
64
- Show installed runtimes and their status.
65
-
66
- ```bash
67
- npx @hua-labs/tap status
68
- ```
69
-
70
- Output shows three status levels:
71
-
72
- - **installed** — config written but not verified
73
- - **configured** — config written and verified
74
- - **active** — runtime is running and connected
75
-
76
- ### `serve`
77
-
78
- Start the tap-comms MCP server (stdio). Convenience command for running the MCP server locally.
79
-
80
- ```bash
81
- npx @hua-labs/tap serve
82
- npx @hua-labs/tap serve --comms-dir /path/to/comms
83
- ```
84
-
85
- Requires `bun`. Uses the bundled MCP server entry from `@hua-labs/tap`, with a repo-local fallback for monorepo checkouts.
86
-
87
- ## Supported Runtimes
88
-
89
- | Runtime | Config | Bridge | Mode |
90
- | ------- | ----------------------- | ---------------------- | ------------------ |
91
- | Claude | `.mcp.json` | native-push (fs.watch) | No daemon needed |
92
- | Codex | `~/.codex/config.toml` | WebSocket bridge | Daemon per session |
93
- | Gemini | `.gemini/settings.json` | polling | No daemon needed |
94
-
95
- ## `--json` Flag
96
-
97
- All commands support `--json` for machine-readable output. Returns a single JSON object to stdout with no human log noise.
98
-
99
- ```bash
100
- npx @hua-labs/tap status --json
101
- ```
102
-
103
- ```json
104
- {
105
- "ok": true,
106
- "command": "status",
107
- "code": "TAP_STATUS_OK",
108
- "message": "2 runtime(s) installed",
109
- "warnings": [],
110
- "data": {
111
- "version": "0.2.2",
112
- "commsDir": "/path/to/comms",
113
- "runtimes": {
114
- "claude": { "status": "active", "bridgeMode": "native-push" },
115
- "codex": { "status": "configured", "bridgeMode": "app-server" }
116
- }
117
- }
118
- }
119
- ```
120
-
121
- Error codes use `TAP_*` prefix: `TAP_ADD_OK`, `TAP_NO_OP`, `TAP_PATCH_FAILED`, etc.
122
-
123
- Exit codes: `0` = ok, `1` = error.
124
-
125
- ## Permissions
126
-
127
- `tap init` auto-configures runtime permissions.
128
-
129
- ### Safe mode (default)
130
-
131
- **Claude**: Adds deny rules to `.claude/settings.local.json` blocking destructive operations (force push, hard reset, rm -rf, etc.).
132
-
133
- **Codex**: Sets `workspace-write` sandbox, `full` network access, trusted project paths, and writable roots in `~/.codex/config.toml`.
134
-
135
- ### Full mode
136
-
137
- ```bash
138
- npx @hua-labs/tap init --permissions full
139
- ```
140
-
141
- **Claude**: Removes tap-managed deny rules. User-added rules preserved.
142
-
143
- **Codex**: Sets `danger-full-access` sandbox. Use on trusted local machines only.
144
-
145
- ## How It Works
146
-
147
- Agents communicate through a shared directory (`comms/`) using markdown files:
148
-
149
- ```
150
- comms/
151
- ├── inbox/ # Agent-to-agent messages
152
- ├── reviews/ # Code review results
153
- ├── findings/ # Out-of-scope discoveries
154
- ├── handoff/ # Session handoff documents
155
- ├── retros/ # Retrospectives
156
- └── archive/ # Archived messages
157
- ```
158
-
159
- Each runtime has an adapter that:
160
-
161
- 1. **Probes** — finds config files, checks runtime installation
162
- 2. **Plans** — determines what patches to apply
163
- 3. **Applies** — backs up and patches config files
164
- 4. **Verifies** — confirms the runtime can read the config
165
-
166
- The adapter contract (`RuntimeAdapter`) is the extension point for adding new runtimes.
167
-
168
- ## Changelog (0.2.2)
169
-
170
- ### Bridge
171
-
172
- - **Auth gateway** — Managed bridge now includes an auth proxy with timing-safe token validation (M99)
173
- - **`--no-auth` flag** — Skip auth gateway for localhost-only setups; app-server listens directly on public port (M102)
174
- - **TUI connect URL** — `bridge start` and `bridge status` output shows where to connect Codex TUI (M102)
175
- - **Identity routing** — Bridge matches inbox messages by both `agentId` and `agentName`; self echo-back filtered by both (M101)
176
- - **Display labels** — Bridge prompts, `tap_who`, and notifications use `name [id]` format (M101)
177
-
178
- ### CLI
179
-
180
- - **`tap doctor`** — Diagnose comms, bridge, message, and MCP issues (M95)
181
- - **`tap doctor --fix`** — Auto-fix common issues with post-fix revalidation (M100)
182
- - **Error codes** — 24 CLI error codes with consistent `TAP_*` prefix (M91)
183
- - **Boot streamline** — Faster CLI startup with agent-name persistence (M92)
184
-
185
- ### Infrastructure
186
-
187
- - **Auto-poll fallback** — Bridge falls back to polling when fs.watch is unavailable (M93)
188
- - **Watcher dedup** — Root-cause fix for duplicate message dispatch (M90)
189
- - **tap-plugin test infra** — In-memory test harness for MCP channel tests (M94)
190
- - **Blind test CI** — Cross-model communication verification framework (M98)
191
-
192
- ## License
193
-
194
- MIT
1
+ # @hua-labs/tap
2
+
3
+ Zero-dependency CLI for cross-model AI agent communication setup.
4
+
5
+ One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
6
+
7
+ ## Quick Start
8
+
9
+ > `bun` is required to run the managed tap MCP server. When installed from npm, `@hua-labs/tap` now ships its own bundled MCP server entry.
10
+
11
+ ```bash
12
+ # 1. Initialize comms directory and state
13
+ npx @hua-labs/tap init
14
+
15
+ # 2. Add runtimes
16
+ npx @hua-labs/tap add claude
17
+ npx @hua-labs/tap add codex
18
+ npx @hua-labs/tap add gemini
19
+
20
+ # 3. Check status
21
+ npx @hua-labs/tap status
22
+ ```
23
+
24
+ Your agents can now communicate through the shared comms directory.
25
+
26
+ ## Commands
27
+
28
+ ### `init`
29
+
30
+ Initialize the comms directory and `.tap-comms/` state.
31
+
32
+ By default, the comms directory is created inside the current repo at `./tap-comms`.
33
+
34
+ ```bash
35
+ npx @hua-labs/tap init
36
+ npx @hua-labs/tap init --comms-dir /path/to/comms
37
+ npx @hua-labs/tap init --permissions safe # default: deny destructive ops
38
+ npx @hua-labs/tap init --permissions full # no restrictions (use with caution)
39
+ npx @hua-labs/tap init --force # re-initialize
40
+ ```
41
+
42
+ ### `add <runtime>`
43
+
44
+ Add a runtime. Probes config, plans patches, applies, and verifies.
45
+
46
+ ```bash
47
+ npx @hua-labs/tap add claude
48
+ npx @hua-labs/tap add codex
49
+ npx @hua-labs/tap add gemini
50
+ npx @hua-labs/tap add claude --force # re-install
51
+ ```
52
+
53
+ ### `remove <runtime>`
54
+
55
+ Remove a runtime and rollback config changes.
56
+
57
+ ```bash
58
+ npx @hua-labs/tap remove claude
59
+ npx @hua-labs/tap remove codex
60
+ ```
61
+
62
+ ### `status`
63
+
64
+ Show installed runtimes and their status.
65
+
66
+ ```bash
67
+ npx @hua-labs/tap status
68
+ ```
69
+
70
+ Output shows three status levels:
71
+
72
+ - **installed** — config written but not verified
73
+ - **configured** — config written and verified
74
+ - **active** — runtime is running and connected
75
+
76
+ ### `serve`
77
+
78
+ Start the tap-comms MCP server (stdio). Convenience command for running the MCP server locally.
79
+
80
+ ```bash
81
+ npx @hua-labs/tap serve
82
+ npx @hua-labs/tap serve --comms-dir /path/to/comms
83
+ ```
84
+
85
+ Requires `bun`. Uses the bundled MCP server entry from `@hua-labs/tap`, with a repo-local fallback for monorepo checkouts.
86
+
87
+ ## Supported Runtimes
88
+
89
+ | Runtime | Config | Bridge | Mode |
90
+ | ------- | ----------------------- | ---------------------- | ------------------ |
91
+ | Claude | `.mcp.json` | native-push (fs.watch) | No daemon needed |
92
+ | Codex | `~/.codex/config.toml` | WebSocket bridge | Daemon per session |
93
+ | Gemini | `.gemini/settings.json` | polling | No daemon needed |
94
+
95
+ ## `--json` Flag
96
+
97
+ All commands support `--json` for machine-readable output. Returns a single JSON object to stdout with no human log noise.
98
+
99
+ ```bash
100
+ npx @hua-labs/tap status --json
101
+ ```
102
+
103
+ ```json
104
+ {
105
+ "ok": true,
106
+ "command": "status",
107
+ "code": "TAP_STATUS_OK",
108
+ "message": "2 runtime(s) installed",
109
+ "warnings": [],
110
+ "data": {
111
+ "version": "0.2.2",
112
+ "commsDir": "/path/to/comms",
113
+ "runtimes": {
114
+ "claude": { "status": "active", "bridgeMode": "native-push" },
115
+ "codex": { "status": "configured", "bridgeMode": "app-server" }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ Error codes use `TAP_*` prefix: `TAP_ADD_OK`, `TAP_NO_OP`, `TAP_PATCH_FAILED`, etc.
122
+
123
+ Exit codes: `0` = ok, `1` = error.
124
+
125
+ ## Permissions
126
+
127
+ `tap init` auto-configures runtime permissions.
128
+
129
+ ### Safe mode (default)
130
+
131
+ **Claude**: Adds deny rules to `.claude/settings.local.json` blocking destructive operations (force push, hard reset, rm -rf, etc.).
132
+
133
+ **Codex**: Sets `workspace-write` sandbox, `full` network access, trusted project paths, and writable roots in `~/.codex/config.toml`.
134
+
135
+ ### Full mode
136
+
137
+ ```bash
138
+ npx @hua-labs/tap init --permissions full
139
+ ```
140
+
141
+ **Claude**: Removes tap-managed deny rules. User-added rules preserved.
142
+
143
+ **Codex**: Sets `danger-full-access` sandbox. Use on trusted local machines only.
144
+
145
+ ## How It Works
146
+
147
+ Agents communicate through a shared directory (`comms/`) using markdown files:
148
+
149
+ ```
150
+ comms/
151
+ ├── inbox/ # Agent-to-agent messages
152
+ ├── reviews/ # Code review results
153
+ ├── findings/ # Out-of-scope discoveries
154
+ ├── handoff/ # Session handoff documents
155
+ ├── retros/ # Retrospectives
156
+ └── archive/ # Archived messages
157
+ ```
158
+
159
+ Each runtime has an adapter that:
160
+
161
+ 1. **Probes** — finds config files, checks runtime installation
162
+ 2. **Plans** — determines what patches to apply
163
+ 3. **Applies** — backs up and patches config files
164
+ 4. **Verifies** — confirms the runtime can read the config
165
+
166
+ The adapter contract (`RuntimeAdapter`) is the extension point for adding new runtimes.
167
+
168
+ ## Changelog (0.2.2)
169
+
170
+ ### Bridge
171
+
172
+ - **Auth gateway** — Managed bridge now includes an auth proxy with timing-safe token validation (M99)
173
+ - **`--no-auth` flag** — Skip auth gateway for localhost-only setups; app-server listens directly on public port (M102)
174
+ - **TUI connect URL** — `bridge start` and `bridge status` output shows where to connect Codex TUI (M102)
175
+ - **Identity routing** — Bridge matches inbox messages by both `agentId` and `agentName`; self echo-back filtered by both (M101)
176
+ - **Display labels** — Bridge prompts, `tap_who`, and notifications use `name [id]` format (M101)
177
+
178
+ ### CLI
179
+
180
+ - **`tap doctor`** — Diagnose comms, bridge, message, and MCP issues (M95)
181
+ - **`tap doctor --fix`** — Auto-fix common issues with post-fix revalidation (M100)
182
+ - **Error codes** — 24 CLI error codes with consistent `TAP_*` prefix (M91)
183
+ - **Boot streamline** — Faster CLI startup with agent-name persistence (M92)
184
+
185
+ ### Infrastructure
186
+
187
+ - **Auto-poll fallback** — Bridge falls back to polling when fs.watch is unavailable (M93)
188
+ - **Watcher dedup** — Root-cause fix for duplicate message dispatch (M90)
189
+ - **tap-plugin test infra** — In-memory test harness for MCP channel tests (M94)
190
+ - **Blind test CI** — Cross-model communication verification framework (M98)
191
+
192
+ ## License
193
+
194
+ MIT
@@ -36,12 +36,21 @@ interface HeadlessWarmupClient {
36
36
  startTurn(inputText: string): Promise<string | null>;
37
37
  refreshCurrentThreadState(): Promise<void>;
38
38
  }
39
+ interface LoadedThreadCandidate {
40
+ id: string;
41
+ cwd: string;
42
+ updatedAt: number;
43
+ statusType: string | null;
44
+ thread: any;
45
+ }
39
46
  interface HeartbeatStoreRecord {
40
47
  id?: string;
41
48
  agent?: string;
42
49
  }
43
50
  type HeartbeatStore = Record<string, HeartbeatStoreRecord>;
44
51
  declare const HEADLESS_WARMUP_PROMPT: string;
52
+ declare function threadCwdMatches(expectedCwd: string, actualCwd: string | null | undefined): boolean;
53
+ declare function chooseLoadedThreadForCwd(cwd: string, threads: LoadedThreadCandidate[]): LoadedThreadCandidate | null;
45
54
  declare function resolveAgentId(preferredAgentName?: string | null): string;
46
55
  declare function recipientMatchesAgent(recipient: string, agentId: string, agentName: string): boolean;
47
56
  declare function isOwnMessageSender(sender: string, agentId: string, agentName: string): boolean;
@@ -53,4 +62,4 @@ declare function maybeBootstrapHeadlessTurn(options: Options, cutoff: Date, clie
53
62
  declare function buildOptions(argv: string[]): Options;
54
63
  declare function main(): Promise<void>;
55
64
 
56
- export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, buildOptions, buildUserInput, isOwnMessageSender, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, waitForTurnCompletion };
65
+ export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, type LoadedThreadCandidate, buildOptions, buildUserInput, chooseLoadedThreadForCwd, isOwnMessageSender, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, threadCwdMatches, waitForTurnCompletion };
@@ -31,6 +31,30 @@ var HEADLESS_WARMUP_PROMPT = [
31
31
  var HEADLESS_WARMUP_TIMEOUT_MS = 3e4;
32
32
  var TURN_COMPLETION_POLL_MS = 250;
33
33
  var TURN_COMPLETION_REFRESH_MS = 1e3;
34
+ function normalizeThreadCwd(cwd) {
35
+ return resolve(cwd).replace(/\\/g, "/").toLowerCase();
36
+ }
37
+ function threadCwdMatches(expectedCwd, actualCwd) {
38
+ if (!actualCwd) {
39
+ return false;
40
+ }
41
+ return normalizeThreadCwd(expectedCwd) === normalizeThreadCwd(actualCwd);
42
+ }
43
+ function chooseLoadedThreadForCwd(cwd, threads) {
44
+ const matching = threads.filter((thread) => threadCwdMatches(cwd, thread.cwd));
45
+ if (matching.length === 0) {
46
+ return null;
47
+ }
48
+ matching.sort((left, right) => {
49
+ const leftActive = left.statusType === "active" ? 1 : 0;
50
+ const rightActive = right.statusType === "active" ? 1 : 0;
51
+ if (leftActive !== rightActive) {
52
+ return rightActive - leftActive;
53
+ }
54
+ return right.updatedAt - left.updatedAt;
55
+ });
56
+ return matching[0] ?? null;
57
+ }
34
58
  function printHelp() {
35
59
  console.log(`Codex App Server bridge
36
60
 
@@ -344,12 +368,13 @@ function readThreadState(stateDir) {
344
368
  }
345
369
  return null;
346
370
  }
347
- function persistThreadState(stateDir, threadId, appServerUrl, ephemeral) {
371
+ function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
348
372
  const payload = {
349
373
  threadId,
350
374
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
351
375
  appServerUrl,
352
- ephemeral
376
+ ephemeral,
377
+ cwd
353
378
  };
354
379
  writeFileSync(
355
380
  join(stateDir, "thread.json"),
@@ -646,7 +671,7 @@ async function waitForTurnCompletion(client, turnId, timeoutMs) {
646
671
  throw new Error(`Timed out waiting for turn ${turnId} to complete`);
647
672
  }
648
673
  async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
649
- if (process.env.TAP_HEADLESS !== "true") {
674
+ if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
650
675
  return false;
651
676
  }
652
677
  const { candidates } = getPendingCandidates(options, cutoff);
@@ -714,6 +739,7 @@ var AppServerClient = class {
714
739
  connected = false;
715
740
  initialized = false;
716
741
  threadId = null;
742
+ currentThreadCwd = null;
717
743
  activeTurnId = null;
718
744
  lastTurnStatus = null;
719
745
  lastNotificationMethod = null;
@@ -797,7 +823,7 @@ var AppServerClient = class {
797
823
  this.initialized = false;
798
824
  this.socket = null;
799
825
  }
800
- async ensureThread(explicitThreadId, resumeThreadId, cwd, ephemeral) {
826
+ async ensureThread(explicitThreadId, savedThread, cwd, ephemeral) {
801
827
  if (explicitThreadId) {
802
828
  try {
803
829
  const resumeResponse = await this.request("thread/resume", {
@@ -820,22 +846,38 @@ var AppServerClient = class {
820
846
  if (loadedThreadId) {
821
847
  return loadedThreadId;
822
848
  }
823
- if (resumeThreadId) {
824
- try {
825
- const resumeResponse = await this.request("thread/resume", {
826
- threadId: resumeThreadId,
827
- persistExtendedHistory: false
828
- });
829
- const resumedThreadId = resumeResponse?.thread?.id ?? resumeThreadId;
830
- await this.refreshThreadState(resumedThreadId);
831
- this.logger(
832
- `resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
833
- );
834
- return resumedThreadId;
835
- } catch (error) {
849
+ if (savedThread?.threadId) {
850
+ if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
836
851
  this.logger(
837
- `saved thread resume failed for ${resumeThreadId}; starting a fresh thread (${String(error)})`
852
+ `saved thread ${savedThread.threadId} cwd ${savedThread.cwd} does not match ${cwd}; skipping saved thread`
838
853
  );
854
+ } else {
855
+ try {
856
+ const resumeResponse = await this.request("thread/resume", {
857
+ threadId: savedThread.threadId,
858
+ persistExtendedHistory: false
859
+ });
860
+ const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
861
+ await this.refreshThreadState(resumedThreadId);
862
+ if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
863
+ this.logger(
864
+ `saved thread ${resumedThreadId} cwd ${this.currentThreadCwd ?? "unknown"} does not match ${cwd}; starting a fresh thread`
865
+ );
866
+ this.threadId = null;
867
+ this.currentThreadCwd = null;
868
+ this.activeTurnId = null;
869
+ this.lastTurnStatus = null;
870
+ } else {
871
+ this.logger(
872
+ `resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
873
+ );
874
+ return resumedThreadId;
875
+ }
876
+ } catch (error) {
877
+ this.logger(
878
+ `saved thread resume failed for ${savedThread.threadId}; starting a fresh thread (${String(error)})`
879
+ );
880
+ }
839
881
  }
840
882
  }
841
883
  const startResponse = await this.request("thread/start", {
@@ -848,7 +890,9 @@ var AppServerClient = class {
848
890
  if (!startedThreadId) {
849
891
  throw new Error("thread/start did not return a thread id");
850
892
  }
893
+ this.syncThreadStateFromThread(startResponse?.thread);
851
894
  this.threadId = startedThreadId;
895
+ this.currentThreadCwd = this.currentThreadCwd ?? cwd;
852
896
  this.activeTurnId = null;
853
897
  this.lastTurnStatus = null;
854
898
  this.logger(`started thread ${startedThreadId}`);
@@ -886,20 +930,13 @@ var AppServerClient = class {
886
930
  continue;
887
931
  }
888
932
  }
889
- const matching = threads.filter((thread) => thread.cwd === cwd);
890
- const candidates = matching.length > 0 ? matching : threads;
891
- if (candidates.length === 0) {
933
+ const chosen = chooseLoadedThreadForCwd(cwd, threads);
934
+ if (!chosen) {
935
+ if (threads.length > 0) {
936
+ this.logger(`loaded threads exist but none match cwd ${cwd}`);
937
+ }
892
938
  return null;
893
939
  }
894
- candidates.sort((left, right) => {
895
- const leftActive = left.statusType === "active" ? 1 : 0;
896
- const rightActive = right.statusType === "active" ? 1 : 0;
897
- if (leftActive !== rightActive) {
898
- return rightActive - leftActive;
899
- }
900
- return right.updatedAt - left.updatedAt;
901
- });
902
- const chosen = candidates[0];
903
940
  this.syncThreadStateFromThread(chosen.thread);
904
941
  this.logger(
905
942
  `attached to loaded thread ${chosen.id}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
@@ -972,6 +1009,7 @@ var AppServerClient = class {
972
1009
  if (typeof thread?.id === "string") {
973
1010
  this.threadId = thread.id;
974
1011
  }
1012
+ this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
975
1013
  let activeTurnId = null;
976
1014
  let lastTurnStatus = null;
977
1015
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
@@ -1020,6 +1058,9 @@ var AppServerClient = class {
1020
1058
  if (params?.thread?.id) {
1021
1059
  this.threadId = params.thread.id;
1022
1060
  }
1061
+ if (typeof params?.thread?.cwd === "string") {
1062
+ this.currentThreadCwd = params.thread.cwd;
1063
+ }
1023
1064
  this.logger(`thread started ${params?.thread?.id ?? ""}`.trim());
1024
1065
  break;
1025
1066
  case "thread/status/changed":
@@ -1075,6 +1116,16 @@ var AppServerClient = class {
1075
1116
  }
1076
1117
  };
1077
1118
  function writeHeartbeat(options, client, health) {
1119
+ if (client?.threadId) {
1120
+ const savedThread = readThreadState(options.stateDir);
1121
+ persistThreadState(
1122
+ options.stateDir,
1123
+ client.threadId,
1124
+ options.appServerUrl,
1125
+ options.ephemeral,
1126
+ client.currentThreadCwd ?? savedThread?.cwd ?? null
1127
+ );
1128
+ }
1078
1129
  const payload = {
1079
1130
  pid: process.pid,
1080
1131
  agent: options.agentName,
@@ -1084,6 +1135,7 @@ function writeHeartbeat(options, client, health) {
1084
1135
  connected: client?.connected ?? false,
1085
1136
  initialized: client?.initialized ?? false,
1086
1137
  threadId: client?.threadId ?? null,
1138
+ threadCwd: client?.currentThreadCwd ?? null,
1087
1139
  activeTurnId: client?.activeTurnId ?? null,
1088
1140
  lastTurnStatus: client?.lastTurnStatus ?? null,
1089
1141
  lastNotificationMethod: client?.lastNotificationMethod ?? null,
@@ -1235,7 +1287,7 @@ async function main() {
1235
1287
  options.messageLookbackMinutes,
1236
1288
  options.processExistingMessages
1237
1289
  );
1238
- const savedThread = readThreadState(options.stateDir);
1290
+ const initialSavedThread = readThreadState(options.stateDir);
1239
1291
  logStatus("codex app-server bridge ready");
1240
1292
  console.log(` repo: ${options.repoRoot}`);
1241
1293
  console.log(` comms: ${options.commsDir}`);
@@ -1251,14 +1303,15 @@ async function main() {
1251
1303
  console.log(
1252
1304
  ` lookback: ${options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`}`
1253
1305
  );
1254
- if (options.threadId || savedThread?.threadId) {
1255
- console.log(` thread: ${options.threadId ?? savedThread?.threadId}`);
1306
+ if (options.threadId || initialSavedThread?.threadId) {
1307
+ console.log(
1308
+ ` thread: ${options.threadId ?? initialSavedThread?.threadId}`
1309
+ );
1256
1310
  }
1257
1311
  if (options.dryRun) {
1258
1312
  logStatus("dry-run mode enabled");
1259
1313
  }
1260
1314
  let client = null;
1261
- let savedThreadId = savedThread?.threadId ?? null;
1262
1315
  const health = {
1263
1316
  consecutiveFailureCount: 0
1264
1317
  };
@@ -1272,9 +1325,10 @@ async function main() {
1272
1325
  options.gatewayToken
1273
1326
  );
1274
1327
  await client.connect();
1328
+ const savedThread = readThreadState(options.stateDir);
1275
1329
  const threadId = await client.ensureThread(
1276
1330
  options.threadId,
1277
- savedThreadId,
1331
+ savedThread,
1278
1332
  options.repoRoot,
1279
1333
  options.ephemeral
1280
1334
  );
@@ -1282,9 +1336,9 @@ async function main() {
1282
1336
  options.stateDir,
1283
1337
  threadId,
1284
1338
  options.appServerUrl,
1285
- options.ephemeral
1339
+ options.ephemeral,
1340
+ client.currentThreadCwd ?? options.repoRoot
1286
1341
  );
1287
- savedThreadId = threadId;
1288
1342
  writeHeartbeat(options, client, health);
1289
1343
  const bootstrapped = await maybeBootstrapHeadlessTurn(
1290
1344
  options,
@@ -1356,6 +1410,7 @@ export {
1356
1410
  HEADLESS_WARMUP_PROMPT,
1357
1411
  buildOptions,
1358
1412
  buildUserInput,
1413
+ chooseLoadedThreadForCwd,
1359
1414
  isOwnMessageSender,
1360
1415
  main,
1361
1416
  maybeBootstrapHeadlessTurn,
@@ -1363,6 +1418,7 @@ export {
1363
1418
  resolveAddressLabel,
1364
1419
  resolveAgentId,
1365
1420
  resolveCurrentAgentName,
1421
+ threadCwdMatches,
1366
1422
  waitForTurnCompletion
1367
1423
  };
1368
1424
  //# sourceMappingURL=codex-app-server-bridge.mjs.map