@hua-labs/tap 0.5.0 → 0.5.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.
package/README.md CHANGED
@@ -194,6 +194,14 @@ The adapter contract (`RuntimeAdapter`) is the extension point for adding new ru
194
194
  - **Bundled MCP runtime** — bundled `.mjs` server entries now prefer `node`; repo-local TypeScript sources still use `bun`
195
195
  - **Hotfixes** — ESM `require()` breakage, temp file leaks in name claims, and claim-stealing edge cases were fixed during publish prep
196
196
 
197
+ ### Trust Layer And Delivery
198
+
199
+ - **Shared vs runtime state split** — `TAP_STATE_DIR` remains the shared source of truth while `TAP_RUNTIME_STATE_DIR` is reserved for per-bridge runtime files, so headless restarts and later TUI attaches keep the same identity contract
200
+ - **Attached TUI rebind** — Codex TUI attach can now recover `agentId` and `agentName` from runtime heartbeat and agent-name files without relying on per-session env injection
201
+ - **State surface alignment** — bridge status, runtime heartbeat, and presence now read from the same state surfaces, reducing mismatches between `tap status`, bridge state, and plugin-visible presence
202
+ - **Broadcast dedupe** — bridge-dispatched notifications are deduplicated so one broadcast does not fan out twice
203
+ - **Ack storm prevention** — peer DM auto-replies are rate-limited to stop acknowledgement loops from flooding the inbox
204
+
197
205
  ### Test Hardening
198
206
 
199
207
  - **CLI-path coverage** — integration tests now exercise the actual `bridge` and `up` command paths that patch Codex `approval_mode`
@@ -205,6 +213,7 @@ The adapter contract (`RuntimeAdapter`) is the extension point for adding new ru
205
213
  - **Bundled MCP command changed for packaged installs** — if your managed `config.toml` still points bundled tap MCP entries at `bun`, rerun `npx @hua-labs/tap add codex --force` or `npx @hua-labs/tap doctor --fix` so bundled `.mjs` entries switch to `node`.
206
214
  - **Repo-local source workflows still use `bun`** — local monorepo or source-checkout paths can still resolve to `.ts` server entries, so keep `bun` installed for development workflows.
207
215
  - **Codex approval mode should be `auto`** — managed Codex installs are expected to end up with `[mcp_servers.tap] approval_mode = "auto"`. `tap doctor --fix` will repair stale managed tables.
216
+ - **Restart Codex bridges after upgrading** — managed bridge launches now export both `TAP_STATE_DIR` and `TAP_RUNTIME_STATE_DIR`; restart existing bridge processes so headless/runtime identity repair is active end-to-end.
208
217
 
209
218
  ## License
210
219
 
@@ -1,4 +1,5 @@
1
1
  type BusyMode = "wait" | "steer";
2
+ type LogLevel = "debug" | "info" | "warn" | "error";
2
3
  interface Options {
3
4
  repoRoot: string;
4
5
  commsDir: string;
@@ -17,9 +18,15 @@ interface Options {
17
18
  gatewayToken: string | null;
18
19
  gatewayTokenFile: string | null;
19
20
  busyMode: BusyMode;
21
+ logLevel: LogLevel;
20
22
  threadId: string | null;
21
23
  ephemeral: boolean;
22
24
  }
25
+ interface InboxRoute {
26
+ sender: string;
27
+ recipient: string;
28
+ subject: string;
29
+ }
23
30
  interface Candidate {
24
31
  markerId: string;
25
32
  filePath: string;
@@ -37,6 +44,35 @@ interface ThreadStateRecord {
37
44
  ephemeral: boolean;
38
45
  cwd?: string | null;
39
46
  }
47
+ interface HeartbeatRecord {
48
+ pid: number;
49
+ agent: string;
50
+ updatedAt: string;
51
+ pollSeconds: number;
52
+ appServerUrl: string;
53
+ authenticated: boolean;
54
+ connected: boolean;
55
+ initialized: boolean;
56
+ threadId: string | null;
57
+ threadCwd?: string | null;
58
+ activeTurnId: string | null;
59
+ turnStartedAt: string | null;
60
+ lastTurnStatus: string | null;
61
+ lastTurnAt?: string | null;
62
+ lastDispatchAt?: string | null;
63
+ idleSince?: string | null;
64
+ turnState?: "active" | "idle" | "waiting-approval" | "disconnected";
65
+ lastNotificationMethod: string | null;
66
+ lastNotificationAt: string | null;
67
+ lastError: string | null;
68
+ lastSuccessfulAppServerAt: string | null;
69
+ lastSuccessfulAppServerMethod: string | null;
70
+ consecutiveFailureCount: number;
71
+ busyMode: BusyMode;
72
+ }
73
+ interface BridgeHealthState {
74
+ consecutiveFailureCount: number;
75
+ }
40
76
  interface HeadlessWarmupClient {
41
77
  activeTurnId: string | null;
42
78
  lastTurnStatus: string | null;
@@ -50,24 +86,219 @@ interface LoadedThreadCandidate {
50
86
  statusType: string | null;
51
87
  thread: any;
52
88
  }
89
+ interface RequestRecord {
90
+ jsonrpc: "2.0";
91
+ id: number;
92
+ method: string;
93
+ params: unknown;
94
+ }
53
95
  interface HeartbeatStoreRecord {
54
96
  id?: string;
55
97
  agent?: string;
98
+ timestamp?: string;
99
+ lastActivity?: string;
100
+ joinedAt?: string;
101
+ status?: string;
102
+ source?: "bridge-dispatch" | "mcp-direct";
103
+ instanceId?: string | null;
104
+ bridgePid?: number | null;
105
+ connectHash?: string;
56
106
  }
57
107
  type HeartbeatStore = Record<string, HeartbeatStoreRecord>;
108
+ interface JsonRpcResponse {
109
+ id?: number;
110
+ result?: any;
111
+ error?: {
112
+ code?: number;
113
+ message?: string;
114
+ data?: unknown;
115
+ };
116
+ method?: string;
117
+ params?: any;
118
+ }
119
+ declare const DEFAULT_AGENT: string;
120
+ declare const DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
121
+ declare const AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
122
+ declare const PLACEHOLDER_AGENT_VALUES: Set<string>;
58
123
  declare const HEADLESS_WARMUP_PROMPT: string;
124
+ declare const HEADLESS_WARMUP_TIMEOUT_MS = 30000;
125
+ declare const TURN_COMPLETION_POLL_MS = 250;
126
+ declare const TURN_COMPLETION_REFRESH_MS = 1000;
127
+ declare const HEADLESS_SKIP_PATTERNS: RegExp[];
128
+ declare const COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2000;
129
+ declare const COMMS_LOCK_STALE_AGE_MS = 10000;
130
+ /** M203: Timeout after which an active turn is considered stale (5 minutes). */
131
+ declare const STALE_TURN_MS: number;
132
+
133
+ /**
134
+ * M206: Re-export canonicalizeAgentId as canonicalize for backward compat.
135
+ */
136
+ declare function canonicalize(id: string): string;
137
+ declare function normalizeThreadCwd(cwd: string): string;
59
138
  declare function threadCwdMatches(expectedCwd: string, actualCwd: string | null | undefined): boolean;
60
139
  declare function chooseLoadedThreadForCwd(cwd: string, threads: LoadedThreadCandidate[]): LoadedThreadCandidate | null;
140
+ declare function normalizeAgentToken(value?: string | null): string | null;
61
141
  declare function resolveAgentId(preferredAgentName?: string | null): string;
62
- declare function loadResumableThreadState(stateDir: string, fallbackAppServerUrl: string): ThreadStateRecord | null;
142
+ declare function resolveAgentName(preferredAgentName: string | null, stateDir: string): string;
143
+ declare function resolveCurrentAgentName(agentId: string, fallbackAgentName: string, heartbeats: HeartbeatStore): string;
144
+ declare function resolveAddressLabel(address: string, heartbeats: HeartbeatStore): string;
145
+ declare function persistAgentName(stateDir: string, agentName: string): void;
146
+ declare function formatAgentLabel(agentIdOrName: string, displayName?: string | null): string;
147
+ /**
148
+ * Resolve the current display name from heartbeats and persist if changed.
149
+ * Returns the resolved name WITHOUT mutating options.agentName — callers
150
+ * should use the return value for the current scan cycle only.
151
+ * This prevents recipient matching from losing the original configured name.
152
+ */
153
+ declare function refreshAgentIdentity(options: Options, heartbeats: HeartbeatStore): string;
154
+ /**
155
+ * M206: Delegate to shared tap-identity helper.
156
+ * Kept as named export for barrel backward compatibility.
157
+ */
63
158
  declare function recipientMatchesAgent(recipient: string, agentId: string, agentName: string): boolean;
159
+ /**
160
+ * M206: Delegate to shared tap-identity helper.
161
+ * Kept as named export for barrel backward compatibility.
162
+ */
64
163
  declare function isOwnMessageSender(sender: string, agentId: string, agentName: string): boolean;
65
- declare function resolveAddressLabel(address: string, heartbeats: HeartbeatStore): string;
66
- declare function resolveCurrentAgentName(agentId: string, fallbackAgentName: string, heartbeats: HeartbeatStore): string;
164
+ /**
165
+ * M203: Check if a turn's activeFlags indicate it cannot accept steer.
166
+ * Returns true if the turn should be treated as not active.
167
+ */
168
+ declare function isTurnStuckOnApproval(activeFlags: string[]): boolean;
169
+ /**
170
+ * M203: Check if a turn has been running longer than the stale threshold.
171
+ */
172
+ declare function isTurnStale(turnStartedAt: string | null, nowMs?: number): boolean;
173
+ declare function shouldRetrySteerAsStart(error: unknown): boolean;
174
+ /**
175
+ * Parse YAML frontmatter from message content for routing.
176
+ * Returns null if no valid frontmatter found.
177
+ */
178
+ declare function parseBridgeFrontmatter(content: string): {
179
+ sender: string;
180
+ recipient: string;
181
+ subject: string;
182
+ } | null;
183
+ /**
184
+ * Strip YAML frontmatter from message content, returning only the body.
185
+ */
186
+ declare function stripBridgeFrontmatter(content: string): string;
187
+ declare function getInboxRoute(fileName: string, body?: string): InboxRoute;
188
+ declare function getInboxRouteFromFilename(fileName: string): InboxRoute;
189
+
190
+ declare function parseArgs(argv: string[]): {
191
+ repoRoot?: string;
192
+ commsDir?: string;
193
+ agentName?: string;
194
+ stateDir?: string;
195
+ pollSeconds?: number;
196
+ reconnectSeconds?: number;
197
+ messageLookbackMinutes?: number;
198
+ processExistingMessages: boolean;
199
+ dryRun: boolean;
200
+ runOnce: boolean;
201
+ waitAfterDispatchSeconds?: number;
202
+ appServerUrl?: string;
203
+ gatewayTokenFile?: string;
204
+ busyMode?: BusyMode;
205
+ logLevel?: LogLevel;
206
+ threadId?: string;
207
+ ephemeral: boolean;
208
+ };
209
+ declare function resolveRepoRoot(explicit?: string): string;
210
+ declare function resolveTapConfigPath(repoRoot: string, input: string): string;
211
+ declare function resolveCommsDir(repoRoot: string, explicit?: string): string;
212
+ declare function resolvePreferredAgentName(requested?: string): string | null;
213
+ declare function sanitizeStateSegment(agentName: string): string;
214
+ declare function buildDefaultStateDir(repoRoot: string, preferredAgentName?: string | null): string;
215
+ declare function resolveStateDir(repoRoot: string, explicit?: string, preferredAgentName?: string | null): string;
216
+ declare function readGatewayTokenFile(tokenFile: string): string;
217
+ declare function buildOptions(argv: string[]): Options;
218
+
219
+ declare function buildMarkerId(filePath: string, mtimeMs: number): string;
220
+ declare function getProcessedMarkerPath(stateDir: string, markerId: string): string;
221
+ declare function loadHeartbeats(commsDir: string): HeartbeatStore;
222
+ declare function shouldSkipInHeadlessMode(fileName: string, body: string): boolean;
223
+ declare function collectCandidates(inboxDir: string, agentId: string, agentName: string, aliasName?: string): Candidate[];
224
+ declare function getPendingCandidates(options: Options, cutoff: Date): {
225
+ heartbeats: HeartbeatStore;
226
+ candidates: Candidate[];
227
+ };
228
+
67
229
  declare function buildUserInput(candidate: Candidate, agentName: string, heartbeats: HeartbeatStore): string;
230
+ declare function writeProcessedMarker(stateDir: string, candidate: Candidate, dispatchMode: "start" | "steer", threadId: string | null, turnId: string | null): void;
231
+ declare function writeLastDispatch(stateDir: string, candidate: Candidate, dispatchMode: "start" | "steer", threadId: string | null, turnId: string | null): void;
232
+
233
+ type LogContext = Record<string, unknown>;
234
+ interface BridgeLogger {
235
+ debug(message: string, context?: LogContext): void;
236
+ info(message: string, context?: LogContext): void;
237
+ warn(message: string, context?: LogContext): void;
238
+ error(message: string, context?: LogContext): void;
239
+ }
240
+
241
+ declare function readSocketData(data: unknown): Promise<string>;
242
+ declare function formatJsonRpcError(error: JsonRpcResponse["error"]): string;
243
+ declare class AppServerClient {
244
+ private socket;
245
+ private readonly url;
246
+ private readonly gatewayToken;
247
+ private readonly logger;
248
+ private readonly clientId;
249
+ private nextId;
250
+ private pending;
251
+ connected: boolean;
252
+ initialized: boolean;
253
+ threadId: string | null;
254
+ currentThreadCwd: string | null;
255
+ activeTurnId: string | null;
256
+ turnStartedAt: string | null;
257
+ lastTurnStatus: string | null;
258
+ lastNotificationMethod: string | null;
259
+ lastNotificationAt: string | null;
260
+ lastError: string | null;
261
+ lastSuccessfulAppServerAt: string | null;
262
+ lastSuccessfulAppServerMethod: string | null;
263
+ constructor(url: string, logger: BridgeLogger, gatewayToken?: string | null);
264
+ connect(): Promise<void>;
265
+ disconnect(): Promise<void>;
266
+ ensureThread(explicitThreadId: string | null, savedThread: ThreadStateRecord | null, cwd: string, ephemeral: boolean): Promise<string>;
267
+ findLoadedThread(cwd: string): Promise<string | null>;
268
+ startTurn(inputText: string): Promise<string | null>;
269
+ steerTurn(inputText: string): Promise<string>;
270
+ isBusy(): boolean;
271
+ refreshCurrentThreadState(): Promise<void>;
272
+ private requireThreadId;
273
+ private requireActiveTurnId;
274
+ private refreshThreadState;
275
+ private syncThreadStateFromThread;
276
+ private handleMessage;
277
+ private handleNotification;
278
+ private request;
279
+ private rejectPending;
280
+ }
281
+
282
+ declare function sanitizeErrorForPersistence(error: string | null): string | null;
283
+ declare function readThreadState(stateDir: string): ThreadStateRecord | null;
284
+ declare function persistThreadState(stateDir: string, threadId: string, appServerUrl: string, ephemeral: boolean, cwd: string | null): void;
285
+ declare function acquireCommsLock(lockPath: string): boolean;
286
+ declare function releaseCommsLock(lockPath: string): void;
287
+ declare function updateCommsHeartbeat(options: Options, status: string): void;
288
+ declare function writeHeartbeat(options: Options, client: AppServerClient | null, health: BridgeHealthState): void;
289
+ declare function dispatchCandidate(client: AppServerClient, options: Options, candidate: Candidate, heartbeats: HeartbeatStore): Promise<boolean>;
290
+ declare function runScan(options: Options, cutoff: Date, client: AppServerClient | null): Promise<{
291
+ dispatched: boolean;
292
+ maxMtimeMs: number;
293
+ }>;
294
+ declare function waitForTurnDrain(options: Options, client: AppServerClient, health: BridgeHealthState): Promise<void>;
68
295
  declare function waitForTurnCompletion(client: Pick<HeadlessWarmupClient, "activeTurnId" | "lastTurnStatus" | "refreshCurrentThreadState">, turnId: string, timeoutMs: number): Promise<string | null>;
69
296
  declare function maybeBootstrapHeadlessTurn(options: Options, cutoff: Date, client: HeadlessWarmupClient): Promise<boolean>;
70
- declare function buildOptions(argv: string[]): Options;
297
+
298
+ declare function readHeartbeatState(stateDir: string): HeartbeatRecord | null;
299
+ declare function loadResumableThreadState(stateDir: string, fallbackAppServerUrl: string): ThreadStateRecord | null;
300
+ declare function getGeneralInboxCutoff(stateDir: string, lookbackMinutes: number, processExistingMessages: boolean): Date;
71
301
  declare function main(): Promise<void>;
302
+ declare function isDirectExecution(): boolean;
72
303
 
73
- export { HEADLESS_WARMUP_PROMPT, type HeadlessWarmupClient, type LoadedThreadCandidate, buildOptions, buildUserInput, chooseLoadedThreadForCwd, isOwnMessageSender, loadResumableThreadState, main, maybeBootstrapHeadlessTurn, recipientMatchesAgent, resolveAddressLabel, resolveAgentId, resolveCurrentAgentName, threadCwdMatches, waitForTurnCompletion };
304
+ export { AUTH_SUBPROTOCOL_PREFIX, AppServerClient, type BridgeHealthState, type BusyMode, COMMS_HEARTBEAT_LOCK_TIMEOUT_MS, COMMS_LOCK_STALE_AGE_MS, type Candidate, DEFAULT_AGENT, DEFAULT_APP_SERVER_URL, HEADLESS_SKIP_PATTERNS, HEADLESS_WARMUP_PROMPT, HEADLESS_WARMUP_TIMEOUT_MS, type HeadlessWarmupClient, type HeartbeatRecord, type HeartbeatStore, type HeartbeatStoreRecord, type InboxRoute, type JsonRpcResponse, type LoadedThreadCandidate, type LogLevel, type Options, PLACEHOLDER_AGENT_VALUES, type RequestRecord, STALE_TURN_MS, TURN_COMPLETION_POLL_MS, TURN_COMPLETION_REFRESH_MS, type ThreadStateRecord, acquireCommsLock, buildDefaultStateDir, buildMarkerId, buildOptions, buildUserInput, canonicalize, chooseLoadedThreadForCwd, collectCandidates, dispatchCandidate, formatAgentLabel, formatJsonRpcError, getGeneralInboxCutoff, getInboxRoute, getInboxRouteFromFilename, getPendingCandidates, getProcessedMarkerPath, isDirectExecution, isOwnMessageSender, isTurnStale, isTurnStuckOnApproval, loadHeartbeats, loadResumableThreadState, main, maybeBootstrapHeadlessTurn, normalizeAgentToken, normalizeThreadCwd, parseArgs, parseBridgeFrontmatter, persistAgentName, persistThreadState, readGatewayTokenFile, readHeartbeatState, readSocketData, readThreadState, recipientMatchesAgent, refreshAgentIdentity, releaseCommsLock, resolveAddressLabel, resolveAgentId, resolveAgentName, resolveCommsDir, resolveCurrentAgentName, resolvePreferredAgentName, resolveRepoRoot, resolveStateDir, resolveTapConfigPath, runScan, sanitizeErrorForPersistence, sanitizeStateSegment, shouldRetrySteerAsStart, shouldSkipInHeadlessMode, stripBridgeFrontmatter, threadCwdMatches, updateCommsHeartbeat, waitForTurnCompletion, waitForTurnDrain, writeHeartbeat, writeLastDispatch, writeProcessedMarker };