@threadbase-sh/streamer 1.53.1 → 1.54.0
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/dist/cli.cjs +113 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +102 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -1
- package/dist/index.d.ts +97 -1
- package/dist/index.js +102 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -169,6 +169,24 @@ declare function isProviderName(value: unknown): value is ProviderName;
|
|
|
169
169
|
declare function isProviderResumable(_provider: string | null | undefined, availabilityResumable: boolean): boolean;
|
|
170
170
|
|
|
171
171
|
type SessionStatus = "running" | "waiting_input" | "idle";
|
|
172
|
+
/**
|
|
173
|
+
* Phase axis *inside* `status === "running"` — what the agent is doing during a
|
|
174
|
+
* turn. Deliberately a separate field rather than new SessionStatus members:
|
|
175
|
+
* VALID_STATUSES rejects unknown values and the store drops sessions outside
|
|
176
|
+
* the requested set, so a new status string would make those sessions vanish
|
|
177
|
+
* from already-shipped apps. Additive fields are safe; additive values in a
|
|
178
|
+
* union a shipped client filters on are not.
|
|
179
|
+
*
|
|
180
|
+
* The full set is defined here even though Codex only ever emits `working`
|
|
181
|
+
* (its status bar is binary — Ready/Working, and claiming otherwise would be
|
|
182
|
+
* invention). Defining it up front keeps a two-valued provider from fixing the
|
|
183
|
+
* field's shape before Claude's richer footer lands. Consumers must ignore an
|
|
184
|
+
* unrecognised value rather than coerce it.
|
|
185
|
+
*
|
|
186
|
+
* This union lives in exactly one place. Two independently-maintained copies of
|
|
187
|
+
* a TUI-derived grammar have already drifted once (tb-mobile PR #647).
|
|
188
|
+
*/
|
|
189
|
+
type AgentPhase = "thinking" | "streaming" | "hooks" | "acting" | "working";
|
|
172
190
|
/**
|
|
173
191
|
* Process-lifetime axis for a managed session (C1 durable session runtime).
|
|
174
192
|
* Orthogonal to SessionStatus — see SessionResponse.lifecycle for why the two
|
|
@@ -233,6 +251,20 @@ interface ManagedSession {
|
|
|
233
251
|
*/
|
|
234
252
|
statusSource?: StatusSource$1;
|
|
235
253
|
statusUpdatedAt?: Date;
|
|
254
|
+
/**
|
|
255
|
+
* Agent phase within a running turn, scraped from the rendered screen.
|
|
256
|
+
* Optional internally (every existing construction site predates it), but
|
|
257
|
+
* managedToResponse emits it unconditionally as `?? null` — on the wire
|
|
258
|
+
* absence must never be a third state, because the client merges session
|
|
259
|
+
* frames and a merge cannot express a removed key.
|
|
260
|
+
*
|
|
261
|
+
* Cleared in markReady() for the running -> waiting_input turn end, which is
|
|
262
|
+
* the only exit a runner observes on screen. Every other way out of `running`
|
|
263
|
+
* — handleExit, putOnHold, failStartup — is enforced by SessionStore
|
|
264
|
+
* .updateManaged() instead: a phase exists only while the status is
|
|
265
|
+
* `running`, so leaving it clears the field.
|
|
266
|
+
*/
|
|
267
|
+
subStatus?: AgentPhase | null;
|
|
236
268
|
filePath?: string;
|
|
237
269
|
resumedFromConversationId?: string;
|
|
238
270
|
/**
|
|
@@ -339,6 +371,28 @@ type WSMessage = {
|
|
|
339
371
|
stage?: Stage | string;
|
|
340
372
|
stalledSinceMs?: number;
|
|
341
373
|
reworkAttempt?: number;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Agent phase changed within a running turn. Scoped to that session's
|
|
377
|
+
* subscribers, like terminal_output and user_message.
|
|
378
|
+
*
|
|
379
|
+
* A minimal frame rather than a SessionResponse copy, deliberately:
|
|
380
|
+
* managedToResponse recomputes `elapsedMs` from `new Date()` on every call
|
|
381
|
+
* for a live session, so a session copy would differ on every tick whether
|
|
382
|
+
* or not the phase changed — and a client that merges frames would get a
|
|
383
|
+
* fresh object identity several times a second, re-rendering every consumer
|
|
384
|
+
* for the whole turn.
|
|
385
|
+
*
|
|
386
|
+
* `phase` is always present and is `null` when there is no phase. Absence
|
|
387
|
+
* must never carry meaning: clients merge session state, and a merge cannot
|
|
388
|
+
* express a removed key, so an omitted field would keep its previous value
|
|
389
|
+
* and the indicator would latch on a finished turn.
|
|
390
|
+
*/
|
|
391
|
+
| {
|
|
392
|
+
type: "session_phase";
|
|
393
|
+
sessionId: string;
|
|
394
|
+
phase: AgentPhase | null;
|
|
395
|
+
updatedAt: string;
|
|
342
396
|
} | {
|
|
343
397
|
type: "session_list";
|
|
344
398
|
sessions: readonly SessionResponse[];
|
|
@@ -504,6 +558,20 @@ interface SessionResponse {
|
|
|
504
558
|
* Live sessions only.
|
|
505
559
|
*/
|
|
506
560
|
permissionMode?: string;
|
|
561
|
+
/**
|
|
562
|
+
* Agent phase within a running turn, scraped from the rendered PTY screen.
|
|
563
|
+
*
|
|
564
|
+
* NOT optional, and always serialised — `null` when there is no phase. A
|
|
565
|
+
* client that merges session frames (`{...prev, ...next}`) cannot express a
|
|
566
|
+
* removed key, so an omitted field would keep its previous value and the
|
|
567
|
+
* indicator would latch on a finished turn. That is the bug tb-mobile PR #647
|
|
568
|
+
* shipped; absence must never carry meaning here.
|
|
569
|
+
*
|
|
570
|
+
* Consequently this must NOT be moved into the `...(x != null && { x })`
|
|
571
|
+
* guard block in managedToResponse: `!= null` catches null and undefined
|
|
572
|
+
* alike and would convert an explicit clear back into absence.
|
|
573
|
+
*/
|
|
574
|
+
subStatus: AgentPhase | null;
|
|
507
575
|
account?: string;
|
|
508
576
|
messageCount?: number;
|
|
509
577
|
preview?: string;
|
|
@@ -630,6 +698,24 @@ interface PTYManagerOptions {
|
|
|
630
698
|
options: PermissionOption[];
|
|
631
699
|
cursor?: number;
|
|
632
700
|
} | null) => void;
|
|
701
|
+
/**
|
|
702
|
+
* Fired when the agent's phase within a running turn changes, including to
|
|
703
|
+
* `null` at turn end. Additive; absent in tests that omit it.
|
|
704
|
+
*
|
|
705
|
+
* Deliberately NOT routed through onStatusChange, even though that callback
|
|
706
|
+
* already exists and is already relayed across the pty-host boundary. Its
|
|
707
|
+
* handler writes a DB row per invocation with no same-status guard, refreshes
|
|
708
|
+
* the scanner index, broadcasts globally, and pokes the APNs and push
|
|
709
|
+
* notifiers — machinery built for a handful of transitions per session, not
|
|
710
|
+
* for a signal that can fire every SCRAPE_THROTTLE_MS.
|
|
711
|
+
*
|
|
712
|
+
* The server must broadcast this to that session's subscribers only
|
|
713
|
+
* (wsHub.broadcastToClients), as a minimal frame rather than a SessionResponse
|
|
714
|
+
* copy: managedToResponse recomputes elapsedMs on every call, so a session
|
|
715
|
+
* copy would differ every tick and re-render every client consumer of that
|
|
716
|
+
* session for the whole turn.
|
|
717
|
+
*/
|
|
718
|
+
onPhaseChange?: (sessionId: string, phase: AgentPhase | null) => void;
|
|
633
719
|
onLiveQuestion?: (sessionId: string, questions: AskQuestion[]) => void;
|
|
634
720
|
onLiveQuestionGone?: (sessionId: string) => void;
|
|
635
721
|
onUserMessage?: (sessionId: string, text: string, ts: number) => void;
|
|
@@ -1942,6 +2028,7 @@ declare class PTYManager implements SessionRunner {
|
|
|
1942
2028
|
private onStatusChange;
|
|
1943
2029
|
private onReady;
|
|
1944
2030
|
private onPermissionChange;
|
|
2031
|
+
private onPhaseChange;
|
|
1945
2032
|
private onLiveQuestion;
|
|
1946
2033
|
private onLiveQuestionGone;
|
|
1947
2034
|
private onUserMessage;
|
|
@@ -1986,6 +2073,15 @@ declare class PTYManager implements SessionRunner {
|
|
|
1986
2073
|
private recheckReadyFromScreen;
|
|
1987
2074
|
private armReadyFallback;
|
|
1988
2075
|
private clearReadyFallback;
|
|
2076
|
+
/**
|
|
2077
|
+
* Record the agent's phase and notify only on a real change.
|
|
2078
|
+
*
|
|
2079
|
+
* The change guard is load-bearing, not an optimisation: the scrape pass runs
|
|
2080
|
+
* on every chunk, so an unguarded setter would fire the callback — and the
|
|
2081
|
+
* WS frame behind it — several times a second for the entire duration of a
|
|
2082
|
+
* turn while reporting the same value.
|
|
2083
|
+
*/
|
|
2084
|
+
private setPhase;
|
|
1989
2085
|
private markReady;
|
|
1990
2086
|
private handleExit;
|
|
1991
2087
|
}
|
|
@@ -2375,4 +2471,4 @@ declare class StreamerServer {
|
|
|
2375
2471
|
private applyLiveSessionSetting;
|
|
2376
2472
|
}
|
|
2377
2473
|
|
|
2378
|
-
export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartForkSessionOptions, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource$1 as StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
|
|
2474
|
+
export { type AgentClient, type AgentClientOpts, type AgentConfig, type AgentPhase, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartForkSessionOptions, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource$1 as StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
|
package/dist/index.d.ts
CHANGED
|
@@ -169,6 +169,24 @@ declare function isProviderName(value: unknown): value is ProviderName;
|
|
|
169
169
|
declare function isProviderResumable(_provider: string | null | undefined, availabilityResumable: boolean): boolean;
|
|
170
170
|
|
|
171
171
|
type SessionStatus = "running" | "waiting_input" | "idle";
|
|
172
|
+
/**
|
|
173
|
+
* Phase axis *inside* `status === "running"` — what the agent is doing during a
|
|
174
|
+
* turn. Deliberately a separate field rather than new SessionStatus members:
|
|
175
|
+
* VALID_STATUSES rejects unknown values and the store drops sessions outside
|
|
176
|
+
* the requested set, so a new status string would make those sessions vanish
|
|
177
|
+
* from already-shipped apps. Additive fields are safe; additive values in a
|
|
178
|
+
* union a shipped client filters on are not.
|
|
179
|
+
*
|
|
180
|
+
* The full set is defined here even though Codex only ever emits `working`
|
|
181
|
+
* (its status bar is binary — Ready/Working, and claiming otherwise would be
|
|
182
|
+
* invention). Defining it up front keeps a two-valued provider from fixing the
|
|
183
|
+
* field's shape before Claude's richer footer lands. Consumers must ignore an
|
|
184
|
+
* unrecognised value rather than coerce it.
|
|
185
|
+
*
|
|
186
|
+
* This union lives in exactly one place. Two independently-maintained copies of
|
|
187
|
+
* a TUI-derived grammar have already drifted once (tb-mobile PR #647).
|
|
188
|
+
*/
|
|
189
|
+
type AgentPhase = "thinking" | "streaming" | "hooks" | "acting" | "working";
|
|
172
190
|
/**
|
|
173
191
|
* Process-lifetime axis for a managed session (C1 durable session runtime).
|
|
174
192
|
* Orthogonal to SessionStatus — see SessionResponse.lifecycle for why the two
|
|
@@ -233,6 +251,20 @@ interface ManagedSession {
|
|
|
233
251
|
*/
|
|
234
252
|
statusSource?: StatusSource$1;
|
|
235
253
|
statusUpdatedAt?: Date;
|
|
254
|
+
/**
|
|
255
|
+
* Agent phase within a running turn, scraped from the rendered screen.
|
|
256
|
+
* Optional internally (every existing construction site predates it), but
|
|
257
|
+
* managedToResponse emits it unconditionally as `?? null` — on the wire
|
|
258
|
+
* absence must never be a third state, because the client merges session
|
|
259
|
+
* frames and a merge cannot express a removed key.
|
|
260
|
+
*
|
|
261
|
+
* Cleared in markReady() for the running -> waiting_input turn end, which is
|
|
262
|
+
* the only exit a runner observes on screen. Every other way out of `running`
|
|
263
|
+
* — handleExit, putOnHold, failStartup — is enforced by SessionStore
|
|
264
|
+
* .updateManaged() instead: a phase exists only while the status is
|
|
265
|
+
* `running`, so leaving it clears the field.
|
|
266
|
+
*/
|
|
267
|
+
subStatus?: AgentPhase | null;
|
|
236
268
|
filePath?: string;
|
|
237
269
|
resumedFromConversationId?: string;
|
|
238
270
|
/**
|
|
@@ -339,6 +371,28 @@ type WSMessage = {
|
|
|
339
371
|
stage?: Stage | string;
|
|
340
372
|
stalledSinceMs?: number;
|
|
341
373
|
reworkAttempt?: number;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Agent phase changed within a running turn. Scoped to that session's
|
|
377
|
+
* subscribers, like terminal_output and user_message.
|
|
378
|
+
*
|
|
379
|
+
* A minimal frame rather than a SessionResponse copy, deliberately:
|
|
380
|
+
* managedToResponse recomputes `elapsedMs` from `new Date()` on every call
|
|
381
|
+
* for a live session, so a session copy would differ on every tick whether
|
|
382
|
+
* or not the phase changed — and a client that merges frames would get a
|
|
383
|
+
* fresh object identity several times a second, re-rendering every consumer
|
|
384
|
+
* for the whole turn.
|
|
385
|
+
*
|
|
386
|
+
* `phase` is always present and is `null` when there is no phase. Absence
|
|
387
|
+
* must never carry meaning: clients merge session state, and a merge cannot
|
|
388
|
+
* express a removed key, so an omitted field would keep its previous value
|
|
389
|
+
* and the indicator would latch on a finished turn.
|
|
390
|
+
*/
|
|
391
|
+
| {
|
|
392
|
+
type: "session_phase";
|
|
393
|
+
sessionId: string;
|
|
394
|
+
phase: AgentPhase | null;
|
|
395
|
+
updatedAt: string;
|
|
342
396
|
} | {
|
|
343
397
|
type: "session_list";
|
|
344
398
|
sessions: readonly SessionResponse[];
|
|
@@ -504,6 +558,20 @@ interface SessionResponse {
|
|
|
504
558
|
* Live sessions only.
|
|
505
559
|
*/
|
|
506
560
|
permissionMode?: string;
|
|
561
|
+
/**
|
|
562
|
+
* Agent phase within a running turn, scraped from the rendered PTY screen.
|
|
563
|
+
*
|
|
564
|
+
* NOT optional, and always serialised — `null` when there is no phase. A
|
|
565
|
+
* client that merges session frames (`{...prev, ...next}`) cannot express a
|
|
566
|
+
* removed key, so an omitted field would keep its previous value and the
|
|
567
|
+
* indicator would latch on a finished turn. That is the bug tb-mobile PR #647
|
|
568
|
+
* shipped; absence must never carry meaning here.
|
|
569
|
+
*
|
|
570
|
+
* Consequently this must NOT be moved into the `...(x != null && { x })`
|
|
571
|
+
* guard block in managedToResponse: `!= null` catches null and undefined
|
|
572
|
+
* alike and would convert an explicit clear back into absence.
|
|
573
|
+
*/
|
|
574
|
+
subStatus: AgentPhase | null;
|
|
507
575
|
account?: string;
|
|
508
576
|
messageCount?: number;
|
|
509
577
|
preview?: string;
|
|
@@ -630,6 +698,24 @@ interface PTYManagerOptions {
|
|
|
630
698
|
options: PermissionOption[];
|
|
631
699
|
cursor?: number;
|
|
632
700
|
} | null) => void;
|
|
701
|
+
/**
|
|
702
|
+
* Fired when the agent's phase within a running turn changes, including to
|
|
703
|
+
* `null` at turn end. Additive; absent in tests that omit it.
|
|
704
|
+
*
|
|
705
|
+
* Deliberately NOT routed through onStatusChange, even though that callback
|
|
706
|
+
* already exists and is already relayed across the pty-host boundary. Its
|
|
707
|
+
* handler writes a DB row per invocation with no same-status guard, refreshes
|
|
708
|
+
* the scanner index, broadcasts globally, and pokes the APNs and push
|
|
709
|
+
* notifiers — machinery built for a handful of transitions per session, not
|
|
710
|
+
* for a signal that can fire every SCRAPE_THROTTLE_MS.
|
|
711
|
+
*
|
|
712
|
+
* The server must broadcast this to that session's subscribers only
|
|
713
|
+
* (wsHub.broadcastToClients), as a minimal frame rather than a SessionResponse
|
|
714
|
+
* copy: managedToResponse recomputes elapsedMs on every call, so a session
|
|
715
|
+
* copy would differ every tick and re-render every client consumer of that
|
|
716
|
+
* session for the whole turn.
|
|
717
|
+
*/
|
|
718
|
+
onPhaseChange?: (sessionId: string, phase: AgentPhase | null) => void;
|
|
633
719
|
onLiveQuestion?: (sessionId: string, questions: AskQuestion[]) => void;
|
|
634
720
|
onLiveQuestionGone?: (sessionId: string) => void;
|
|
635
721
|
onUserMessage?: (sessionId: string, text: string, ts: number) => void;
|
|
@@ -1942,6 +2028,7 @@ declare class PTYManager implements SessionRunner {
|
|
|
1942
2028
|
private onStatusChange;
|
|
1943
2029
|
private onReady;
|
|
1944
2030
|
private onPermissionChange;
|
|
2031
|
+
private onPhaseChange;
|
|
1945
2032
|
private onLiveQuestion;
|
|
1946
2033
|
private onLiveQuestionGone;
|
|
1947
2034
|
private onUserMessage;
|
|
@@ -1986,6 +2073,15 @@ declare class PTYManager implements SessionRunner {
|
|
|
1986
2073
|
private recheckReadyFromScreen;
|
|
1987
2074
|
private armReadyFallback;
|
|
1988
2075
|
private clearReadyFallback;
|
|
2076
|
+
/**
|
|
2077
|
+
* Record the agent's phase and notify only on a real change.
|
|
2078
|
+
*
|
|
2079
|
+
* The change guard is load-bearing, not an optimisation: the scrape pass runs
|
|
2080
|
+
* on every chunk, so an unguarded setter would fire the callback — and the
|
|
2081
|
+
* WS frame behind it — several times a second for the entire duration of a
|
|
2082
|
+
* turn while reporting the same value.
|
|
2083
|
+
*/
|
|
2084
|
+
private setPhase;
|
|
1989
2085
|
private markReady;
|
|
1990
2086
|
private handleExit;
|
|
1991
2087
|
}
|
|
@@ -2375,4 +2471,4 @@ declare class StreamerServer {
|
|
|
2375
2471
|
private applyLiveSessionSetting;
|
|
2376
2472
|
}
|
|
2377
2473
|
|
|
2378
|
-
export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartForkSessionOptions, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource$1 as StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
|
|
2474
|
+
export { type AgentClient, type AgentClientOpts, type AgentConfig, type AgentPhase, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionLifecycle, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartForkSessionOptions, type StartFreshSessionOptions, type StartSessionOptions, type StatusConfidence, type StatusSource$1 as StatusSource, StreamerServer, type UserMessage, WSHub, type WSMessage, confidenceForSource, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
|
package/dist/index.js
CHANGED
|
@@ -1061,6 +1061,7 @@ function rememberedGateDigit(gate) {
|
|
|
1061
1061
|
// src/services/questions/codexScreen.ts
|
|
1062
1062
|
var CODEX_PROMPT_READY_TEXT = "Ready";
|
|
1063
1063
|
var CODEX_BUSY_STATUS_RE = /\b(?:Starting|Working)\b/;
|
|
1064
|
+
var CODEX_WORKING_STATUS_RE = /\bWorking\b/;
|
|
1064
1065
|
var CODEX_MCP_BOOT_RE = /Booting MCP|Starting MCP servers/i;
|
|
1065
1066
|
var CODEX_TRUST_GATE_REGEX = /trust the contents/i;
|
|
1066
1067
|
var CODEX_HOOKS_GATE_REGEX = /hooks need review/i;
|
|
@@ -1097,12 +1098,15 @@ function detectCodexBlockingPrompt(lines) {
|
|
|
1097
1098
|
function codexStatusBarLine(lines) {
|
|
1098
1099
|
return [...lines].reverse().find((l) => l.trim() !== "") ?? "";
|
|
1099
1100
|
}
|
|
1100
|
-
function
|
|
1101
|
+
function codexScreenPreTurn(lines) {
|
|
1101
1102
|
const screenText = lines.join("\n");
|
|
1102
1103
|
if (CODEX_HOOKS_GATE_REGEX.test(screenText) || CODEX_TRUST_GATE_REGEX.test(screenText)) {
|
|
1103
1104
|
return true;
|
|
1104
1105
|
}
|
|
1105
|
-
|
|
1106
|
+
return CODEX_MCP_BOOT_RE.test(screenText);
|
|
1107
|
+
}
|
|
1108
|
+
function codexScreenBlocksComposer(lines) {
|
|
1109
|
+
if (codexScreenPreTurn(lines)) return true;
|
|
1106
1110
|
return CODEX_BUSY_STATUS_RE.test(codexStatusBarLine(lines));
|
|
1107
1111
|
}
|
|
1108
1112
|
function codexScreenShowsReady(lines) {
|
|
@@ -1149,6 +1153,20 @@ function gateCard(gate, lines) {
|
|
|
1149
1153
|
};
|
|
1150
1154
|
}
|
|
1151
1155
|
|
|
1156
|
+
// src/services/questions/parseAgentPhase.ts
|
|
1157
|
+
function codexPhase(lines) {
|
|
1158
|
+
if (codexScreenPreTurn(lines)) return null;
|
|
1159
|
+
return CODEX_WORKING_STATUS_RE.test(codexStatusBarLine(lines)) ? "working" : null;
|
|
1160
|
+
}
|
|
1161
|
+
function claudePhase(_lines) {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
function parseAgentPhase(lines, provider) {
|
|
1165
|
+
if (provider === CODEX_CLI_PROVIDER) return codexPhase(lines);
|
|
1166
|
+
if (provider === CLAUDE_CODE_PROVIDER) return claudePhase(lines);
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1152
1170
|
// src/utils/debounce.ts
|
|
1153
1171
|
function debounce(fn, waitMs) {
|
|
1154
1172
|
let timer = null;
|
|
@@ -1193,6 +1211,7 @@ var CodexPtyRunner = class {
|
|
|
1193
1211
|
sessions = /* @__PURE__ */ new Map();
|
|
1194
1212
|
onOutput;
|
|
1195
1213
|
onStatusChange;
|
|
1214
|
+
onPhaseChange;
|
|
1196
1215
|
onReady;
|
|
1197
1216
|
// Broadcasts Codex's blocking startup gates (directory trust, hooks review)
|
|
1198
1217
|
// as question cards; null dismisses the card once the gate leaves the screen.
|
|
@@ -1241,6 +1260,7 @@ var CodexPtyRunner = class {
|
|
|
1241
1260
|
constructor(options = {}) {
|
|
1242
1261
|
this.onOutput = options.onOutput;
|
|
1243
1262
|
this.onStatusChange = options.onStatusChange;
|
|
1263
|
+
this.onPhaseChange = options.onPhaseChange;
|
|
1244
1264
|
this.onReady = options.onReady;
|
|
1245
1265
|
this.onPermissionChange = options.onPermissionChange;
|
|
1246
1266
|
this.onLiveQuestion = options.onLiveQuestion;
|
|
@@ -1808,6 +1828,9 @@ var CodexPtyRunner = class {
|
|
|
1808
1828
|
);
|
|
1809
1829
|
return;
|
|
1810
1830
|
}
|
|
1831
|
+
if (session.status === "running") {
|
|
1832
|
+
this.setPhase(sessionId, session, parseAgentPhase(lines, CODEX_CLI_PROVIDER));
|
|
1833
|
+
}
|
|
1811
1834
|
const gate = CODEX_HOOKS_GATE_REGEX.test(screenText) ? "hooks" : CODEX_TRUST_GATE_REGEX.test(screenText) ? "trust" : null;
|
|
1812
1835
|
if (gate) {
|
|
1813
1836
|
this.handleGate(sessionId, session, gate, lines);
|
|
@@ -1922,11 +1945,25 @@ var CodexPtyRunner = class {
|
|
|
1922
1945
|
});
|
|
1923
1946
|
this.onPermissionChange?.(sessionId, card);
|
|
1924
1947
|
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Record the agent's phase and notify only on a real change. The guard is
|
|
1950
|
+
* load-bearing, not an optimisation: detectScreenState runs on every chunk,
|
|
1951
|
+
* so an unguarded setter would fire the WS frame behind this several times a
|
|
1952
|
+
* second for a whole turn while reporting the same value. Same contract as
|
|
1953
|
+
* PTYManager.setPhase — the two runners deliberately stay separate classes.
|
|
1954
|
+
*/
|
|
1955
|
+
setPhase(sessionId, session, phase) {
|
|
1956
|
+
const next = phase ?? null;
|
|
1957
|
+
if ((session.subStatus ?? null) === next) return;
|
|
1958
|
+
session.subStatus = next;
|
|
1959
|
+
this.onPhaseChange?.(sessionId, next);
|
|
1960
|
+
}
|
|
1925
1961
|
markReady(sessionId, session, source, reason) {
|
|
1926
1962
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
1927
1963
|
session.status = "waiting_input";
|
|
1928
1964
|
session.statusSource = source;
|
|
1929
1965
|
session.statusUpdatedAt = /* @__PURE__ */ new Date();
|
|
1966
|
+
this.setPhase(sessionId, session, null);
|
|
1930
1967
|
this.log.info(`[codex.ready] ${sessionId.slice(0, 8)} ${reason}`, {
|
|
1931
1968
|
event: "codex.ready",
|
|
1932
1969
|
sessionId,
|
|
@@ -2018,12 +2055,16 @@ function toPublicSession(s) {
|
|
|
2018
2055
|
...s.lastActivityAt != null && { lastActivityAt: s.lastActivityAt },
|
|
2019
2056
|
...s.statusSource != null && { statusSource: s.statusSource },
|
|
2020
2057
|
...s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt },
|
|
2021
|
-
...s.filePath != null && { filePath: s.filePath }
|
|
2058
|
+
...s.filePath != null && { filePath: s.filePath },
|
|
2059
|
+
// Unconditional — see PTYManager's toPublicSession: a streamer re-adopting
|
|
2060
|
+
// a surviving pty-host's sessions mid-turn has no other source for the
|
|
2061
|
+
// phase, and the host's change guard will not re-emit it.
|
|
2062
|
+
subStatus: s.subStatus ?? null
|
|
2022
2063
|
};
|
|
2023
2064
|
}
|
|
2024
2065
|
|
|
2025
2066
|
// src/pty-host/protocol.ts
|
|
2026
|
-
var PTY_HOST_PROTOCOL_VERSION =
|
|
2067
|
+
var PTY_HOST_PROTOCOL_VERSION = 3;
|
|
2027
2068
|
function isHostEvent(message) {
|
|
2028
2069
|
return "type" in message && message.type === "event";
|
|
2029
2070
|
}
|
|
@@ -2261,6 +2302,9 @@ var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
|
2261
2302
|
case "permission-change":
|
|
2262
2303
|
this.options.onPermissionChange?.(event.sessionId, event.gate);
|
|
2263
2304
|
break;
|
|
2305
|
+
case "phase-change":
|
|
2306
|
+
this.options.onPhaseChange?.(event.sessionId, event.phase);
|
|
2307
|
+
break;
|
|
2264
2308
|
case "live-question":
|
|
2265
2309
|
this.options.onLiveQuestion?.(event.sessionId, event.questions);
|
|
2266
2310
|
break;
|
|
@@ -2659,6 +2703,7 @@ var PTYManager = class {
|
|
|
2659
2703
|
onStatusChange;
|
|
2660
2704
|
onReady;
|
|
2661
2705
|
onPermissionChange;
|
|
2706
|
+
onPhaseChange;
|
|
2662
2707
|
onLiveQuestion;
|
|
2663
2708
|
onLiveQuestionGone;
|
|
2664
2709
|
onUserMessage;
|
|
@@ -2720,6 +2765,7 @@ var PTYManager = class {
|
|
|
2720
2765
|
this.onStatusChange = options.onStatusChange;
|
|
2721
2766
|
this.onReady = options.onReady;
|
|
2722
2767
|
this.onPermissionChange = options.onPermissionChange;
|
|
2768
|
+
this.onPhaseChange = options.onPhaseChange;
|
|
2723
2769
|
this.onLiveQuestion = options.onLiveQuestion;
|
|
2724
2770
|
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
2725
2771
|
this.onUserMessage = options.onUserMessage;
|
|
@@ -3226,6 +3272,10 @@ var PTYManager = class {
|
|
|
3226
3272
|
}
|
|
3227
3273
|
this.lastDetectAt.set(sessionId, nowMs);
|
|
3228
3274
|
const lines = await this.getOutputLines(sessionId, 60);
|
|
3275
|
+
const phaseSession = this.sessions.get(sessionId);
|
|
3276
|
+
if (phaseSession?.status === "running") {
|
|
3277
|
+
this.setPhase(sessionId, phaseSession, parseAgentPhase(lines, CLAUDE_CODE_PROVIDER));
|
|
3278
|
+
}
|
|
3229
3279
|
const askFooterOnScreen = lines.some((l) => /Enter to select/i.test(l));
|
|
3230
3280
|
if (oscPermission || hasAskFooter || askFooterOnScreen) {
|
|
3231
3281
|
this.log.debug?.(`[pty.prompt_detect] ${sessionId.slice(0, 8)} trigger`, {
|
|
@@ -3369,12 +3419,27 @@ var PTYManager = class {
|
|
|
3369
3419
|
}
|
|
3370
3420
|
// Transition a session from "running" to "waiting_input", clear pendingReady,
|
|
3371
3421
|
// and flush any queued input. Idempotent: callers can invoke at any chunk.
|
|
3422
|
+
/**
|
|
3423
|
+
* Record the agent's phase and notify only on a real change.
|
|
3424
|
+
*
|
|
3425
|
+
* The change guard is load-bearing, not an optimisation: the scrape pass runs
|
|
3426
|
+
* on every chunk, so an unguarded setter would fire the callback — and the
|
|
3427
|
+
* WS frame behind it — several times a second for the entire duration of a
|
|
3428
|
+
* turn while reporting the same value.
|
|
3429
|
+
*/
|
|
3430
|
+
setPhase(sessionId, session, phase) {
|
|
3431
|
+
const next = phase ?? null;
|
|
3432
|
+
if ((session.subStatus ?? null) === next) return;
|
|
3433
|
+
session.subStatus = next;
|
|
3434
|
+
this.onPhaseChange?.(sessionId, next);
|
|
3435
|
+
}
|
|
3372
3436
|
markReady(sessionId, session, source, reason) {
|
|
3373
3437
|
this.clearReadyFallback(sessionId);
|
|
3374
3438
|
session.lastActivityAt = /* @__PURE__ */ new Date();
|
|
3375
3439
|
session.status = "waiting_input";
|
|
3376
3440
|
session.statusSource = source;
|
|
3377
3441
|
session.statusUpdatedAt = /* @__PURE__ */ new Date();
|
|
3442
|
+
this.setPhase(sessionId, session, null);
|
|
3378
3443
|
const elapsedMs = Date.now() - (this.firstChunkAt.get(sessionId) ?? Date.now());
|
|
3379
3444
|
this.log.info(`[pty.ready] ${sessionId.slice(0, 8)} ${reason} (elapsed=${elapsedMs}ms)`, {
|
|
3380
3445
|
event: "pty.ready",
|
|
@@ -3438,7 +3503,12 @@ function toPublicSession2(s) {
|
|
|
3438
3503
|
...s.statusUpdatedAt != null && { statusUpdatedAt: s.statusUpdatedAt },
|
|
3439
3504
|
...s.filePath != null && { filePath: s.filePath },
|
|
3440
3505
|
...s.sessionName != null && { sessionName: s.sessionName },
|
|
3441
|
-
...s.firstMessageText != null && { firstMessageText: s.firstMessageText }
|
|
3506
|
+
...s.firstMessageText != null && { firstMessageText: s.firstMessageText },
|
|
3507
|
+
// Unconditional: this shape crosses the pty-host boundary, and a streamer
|
|
3508
|
+
// re-adopting a surviving host's sessions mid-turn has no other source for
|
|
3509
|
+
// the phase — no snapshot or replay event carries it, and setPhase's change
|
|
3510
|
+
// guard means the host will never re-emit it for the rest of that turn.
|
|
3511
|
+
subStatus: s.subStatus ?? null
|
|
3442
3512
|
};
|
|
3443
3513
|
}
|
|
3444
3514
|
|
|
@@ -7910,6 +7980,12 @@ function conversationToResumableSession(c) {
|
|
|
7910
7980
|
branch: c.branch ?? void 0,
|
|
7911
7981
|
lastOutput: "",
|
|
7912
7982
|
elapsedMs: 0,
|
|
7983
|
+
// No PTY behind a cached conversation, so there is no phase — emitted
|
|
7984
|
+
// explicitly for the same reason as in managedToResponse/discoveredToResponse:
|
|
7985
|
+
// the client merges session frames, so an absent key keeps the previous
|
|
7986
|
+
// value and the indicator latches. `GET /api/sessions/:id` serves this
|
|
7987
|
+
// shape whenever the id is a conversation rather than a live session.
|
|
7988
|
+
subStatus: null,
|
|
7913
7989
|
promptCount: c.messageCount,
|
|
7914
7990
|
startedAt: c.lastActivity,
|
|
7915
7991
|
completedAt: null,
|
|
@@ -11868,6 +11944,15 @@ function createLiveSessionOptions(deps) {
|
|
|
11868
11944
|
seq
|
|
11869
11945
|
});
|
|
11870
11946
|
},
|
|
11947
|
+
onPhaseChange: (sessionId, phase) => {
|
|
11948
|
+
deps.sessionStore.updateManaged(sessionId, { subStatus: phase });
|
|
11949
|
+
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {
|
|
11950
|
+
type: "session_phase",
|
|
11951
|
+
sessionId,
|
|
11952
|
+
phase,
|
|
11953
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11954
|
+
});
|
|
11955
|
+
},
|
|
11871
11956
|
onUserMessage: (sessionId, text, ts) => {
|
|
11872
11957
|
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(sessionId) ?? [], {
|
|
11873
11958
|
type: "user_message",
|
|
@@ -13897,6 +13982,7 @@ var SessionStore = class {
|
|
|
13897
13982
|
const session = this.managed.get(sessionId);
|
|
13898
13983
|
if (!session) return null;
|
|
13899
13984
|
Object.assign(session, updates);
|
|
13985
|
+
if (updates.status != null && updates.status !== "running") session.subStatus = null;
|
|
13900
13986
|
return session;
|
|
13901
13987
|
}
|
|
13902
13988
|
removeManaged(sessionId) {
|
|
@@ -14077,6 +14163,13 @@ function managedToResponse(s, ptyAttached) {
|
|
|
14077
14163
|
promptCount: s.promptCount,
|
|
14078
14164
|
startedAt: s.startedAt.toISOString(),
|
|
14079
14165
|
completedAt: s.completedAt?.toISOString() ?? null,
|
|
14166
|
+
// Unconditional, like completedAt above — do NOT move this into the
|
|
14167
|
+
// `...(x != null && { x })` block below. That guard uses loose `!=`, which
|
|
14168
|
+
// catches null as well as undefined, and would turn an explicit "no phase"
|
|
14169
|
+
// back into an absent key. The client merges session frames, so an absent
|
|
14170
|
+
// key keeps the previous value and the indicator latches on a finished
|
|
14171
|
+
// turn — the tb-mobile PR #647 bug, arriving through the serialiser.
|
|
14172
|
+
subStatus: s.subStatus ?? null,
|
|
14080
14173
|
ptyAttached,
|
|
14081
14174
|
...s.projectId != null && { projectId: s.projectId },
|
|
14082
14175
|
...s.sessionName != null && { sessionName: s.sessionName },
|
|
@@ -14127,6 +14220,10 @@ function discoveredToResponse(d, conversationId) {
|
|
|
14127
14220
|
// cannot see the process's prompt state.
|
|
14128
14221
|
lifecycle: "detached",
|
|
14129
14222
|
lifecycleSource: "probe",
|
|
14223
|
+
// No PTY here, so nothing to scrape and no phase to report. Emitted
|
|
14224
|
+
// explicitly rather than omitted, for the same reason as in
|
|
14225
|
+
// managedToResponse: absence must never be a third state on the wire.
|
|
14226
|
+
subStatus: null,
|
|
14130
14227
|
projectPath: d.projectPath,
|
|
14131
14228
|
projectName: d.projectName,
|
|
14132
14229
|
branch: d.branch,
|