-
Notifications
You must be signed in to change notification settings - Fork 12
trace-needs-input-marker -> main #951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
79e2549
0dbcf60
6b5dcd4
a026232
d709483
a2cd5d7
1e7d423
bad6ab2
229d316
1f460b2
7c27d3d
2103443
1acadf8
c8de87f
2ace3e0
67b47f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -688,9 +688,23 @@ export async function createAdeRuntime(args: { | |
| laneServiceRef = laneService; | ||
| await laneService.ensurePrimaryLane(); | ||
|
|
||
| // Late-bound because the publisher is constructed after the session/PTY | ||
| // services. Session changes still use it once publishing is attached. | ||
| let pushPublisherForPtySignals: PushPublisherService | null = null; | ||
| const sessionService = createSessionService({ db }); | ||
| sessionService.onChanged((event) => { | ||
| pushEvent("runtime", { type: "terminal_session_changed", event }); | ||
| const session = sessionService.get(event.sessionId); | ||
| if ( | ||
| session | ||
| && (session.status !== "running" || session.runtimeState === "idle") | ||
| && ( | ||
| session.settleOverride === "settled" | ||
| || (session.settleOverride !== "active" && Boolean(session.settledAt)) | ||
|
Comment on lines
+703
to
+708
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This listener runs after every session metadata change, so a chat that retains AGENTS.md reference: AGENTS.md:L35-L35 Useful? React with 👍 / 👎. |
||
| ) | ||
| ) { | ||
| pushPublisherForPtySignals?.handleSessionSettled(projectId, event.sessionId); | ||
| } | ||
| }); | ||
| const processRegistry = createProcessRegistryService({ | ||
| db, | ||
|
|
@@ -900,10 +914,8 @@ export async function createAdeRuntime(args: { | |
| // pattern as desktop main. Without this bridge, paired phones only ever | ||
| // receive terminal snapshots, never live terminal_data push. | ||
| let syncServiceForPtyEvents: ReturnType<typeof createSyncService> | null = null; | ||
| // Same late-binding for the push publisher: it feeds tracked CLI runtime | ||
| // states (running / waiting-input from OSC 133 markers) into the phone's | ||
| // Live Activity, and it's constructed after ptyService. | ||
| let pushPublisherForPtySignals: PushPublisherService | null = null; | ||
| // The late-bound push publisher feeds tracked CLI runtime states into the | ||
| // phone's Live Activity. | ||
| const ptyService = createPtyService({ | ||
| projectRoot, | ||
| transcriptsDir: paths.transcriptsDir, | ||
|
|
@@ -931,6 +943,9 @@ export async function createAdeRuntime(args: { | |
| runtimeState: signal.runtimeState, | ||
| }); | ||
| }, | ||
| onSessionUserInput: ({ sessionId }) => { | ||
| pushPublisherForPtySignals?.handleSessionAttentionResolved(projectId, sessionId); | ||
| }, | ||
| diskPressureMonitor, | ||
| onSessionEnded: (event) => { | ||
| void sessionDeltaService.computeSessionDelta(event.sessionId).catch((error) => { | ||
|
|
@@ -1509,6 +1524,10 @@ export async function createAdeRuntime(args: { | |
| title: session.title ?? null, | ||
| toolType: session.toolType ?? null, | ||
| chatSessionId: session.chatSessionId ?? null, | ||
| status: session.status, | ||
| runtimeState: session.runtimeState ?? null, | ||
| settledAt: session.settledAt ?? null, | ||
| settleOverride: session.settleOverride ?? null, | ||
| }; | ||
| } catch { | ||
| return null; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -196,6 +196,10 @@ export type PushPublisherSources = { | |
| title: string | null; | ||
| toolType?: string | null; | ||
| chatSessionId?: string | null; | ||
| status?: string | null; | ||
| runtimeState?: string | null; | ||
| settledAt?: string | null; | ||
| settleOverride?: "settled" | "active" | null; | ||
| } | null; | ||
| }; | ||
|
|
||
|
|
@@ -932,6 +936,19 @@ export function createPushPublisherService(deps: PushPublisherDeps) { | |
| runs.delete(run.sessionId); | ||
| continue; | ||
| } | ||
| const atRest = record.status !== "running" || record.runtimeState === "idle"; | ||
| if ( | ||
| atRest | ||
| && ( | ||
| record.settleOverride === "settled" | ||
| || (record.settleOverride !== "active" && record.settledAt) | ||
| ) | ||
| ) { | ||
| run.phase = "completed"; | ||
| recentRuns.set(run.sessionId, { ...run }); | ||
| runs.delete(run.sessionId); | ||
| continue; | ||
| } | ||
| run.title = record.title?.trim() || run.title || null; | ||
| run.agent = providerDisplayName(record.toolType) ?? run.agent ?? "CLI"; | ||
| run.metaResolved = true; | ||
|
|
@@ -1548,22 +1565,49 @@ export function createPushPublisherService(deps: PushPublisherDeps) { | |
| }; | ||
|
|
||
| /** | ||
| * OSC 133-derived terminal state for tracked CLI sessions. Feeds the Live | ||
| * Activity only — no alert pushes: a CLI agent returns to its prompt | ||
| * (waiting-input) after EVERY turn, so alerting on it would ping the user | ||
| * once per turn. Failure alerts stay with onPtyExit's non-zero-exit path. | ||
| * Terminal runtime state for tracked CLI sessions. Prompt/marker inference | ||
| * must never raise attention: only an explicit `ade chat ask` or a | ||
| * provider-structured pending input may publish `waiting_for_input`. | ||
| */ | ||
| const onCliRuntimeSignal = (scopeKey: string, signal: PushCliRuntimeSignal): void => { | ||
| if (disposed || !signal.sessionId) return; | ||
| const existing = runs.get(signal.sessionId); | ||
| const session = scopes.get(scopeKey)?.resolveCliSession?.(signal.sessionId) ?? null; | ||
| const atRest = session?.status !== "running" || signal.runtimeState === "idle"; | ||
| if ( | ||
| atRest | ||
| && ( | ||
| session?.settleOverride === "settled" | ||
| || (session?.settleOverride !== "active" && Boolean(session?.settledAt)) | ||
| ) | ||
| ) { | ||
| if (existing) { | ||
| existing.phase = "completed"; | ||
| existing.itemId = null; | ||
| markRunUpdated(existing); | ||
| recentRuns.set(signal.sessionId, { ...existing }); | ||
| runs.delete(signal.sessionId); | ||
| pendingAlerts = pendingAlerts.filter( | ||
| (alert) => | ||
| alert.dedupeKey !== `alert:${signal.sessionId}:approval` | ||
| && alert.dedupeKey !== `alert:${signal.sessionId}:question`, | ||
| ); | ||
| clearAlertDedupe(`alert:${signal.sessionId}:approval`); | ||
| clearAlertDedupe(`alert:${signal.sessionId}:question`); | ||
| scheduleFlush(true); | ||
| } | ||
| return; | ||
| } | ||
| // Exit/kill phases are owned by onPtyExit (which knows the exit code). | ||
| if (signal.runtimeState === "exited" || signal.runtimeState === "killed") return; | ||
| // Explicit/provider-structured attention owns this phase until the | ||
| // lifecycle event that resolves it. PTY heartbeats are observational and | ||
| // must not erase a real request. | ||
| if (existing?.phase === "waiting_for_input" || existing?.phase === "waiting_for_approval") return; | ||
|
arul28 marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an explicit CLI ask is cleared through AGENTS.md reference: AGENTS.md:L35-L35 Useful? React with 👍 / 👎. |
||
| // `idle` = no output for 12s with no OSC prompt marker — we can't prove | ||
| // the CLI is working OR at a prompt, so publish it as `stale` (dimmed, | ||
| // not counted active) instead of overstating it as a live running row. | ||
| const phase: AgentRunPhase = signal.runtimeState === "waiting-input" | ||
| ? "waiting_for_input" | ||
| : signal.runtimeState === "idle" | ||
| const phase: AgentRunPhase = signal.runtimeState === "waiting-input" || signal.runtimeState === "idle" | ||
| ? "stale" | ||
| : "running"; | ||
| // Signals re-fire on a ~10s heartbeat; only a phase change is worth a | ||
|
|
@@ -1886,6 +1930,43 @@ export function createPushPublisherService(deps: PushPublisherDeps) { | |
| scheduleFlush(true); | ||
| }, | ||
|
|
||
| handleSessionAttentionResolved(scopeKey: string | null, sessionId: string): void { | ||
| if (disposed || !sessionId) return; | ||
| const run = runs.get(sessionId); | ||
| if (!run || (scopeKey != null && run.scopeKey !== scopeKey)) return; | ||
| if (run.phase !== "waiting_for_input" && run.phase !== "waiting_for_approval") return; | ||
| run.phase = "running"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the socket AGENTS.md reference: AGENTS.md:L35-L35 Useful? React with 👍 / 👎. |
||
| run.itemId = null; | ||
| markRunUpdated(run); | ||
| pendingAlerts = pendingAlerts.filter( | ||
| (alert) => | ||
| alert.dedupeKey !== `alert:${sessionId}:approval` | ||
| && alert.dedupeKey !== `alert:${sessionId}:question`, | ||
| ); | ||
| clearAlertDedupe(`alert:${sessionId}:approval`); | ||
| clearAlertDedupe(`alert:${sessionId}:question`); | ||
| scheduleFlush(true); | ||
| }, | ||
|
|
||
| handleSessionSettled(scopeKey: string | null, sessionId: string): void { | ||
| if (disposed || !sessionId) return; | ||
| const run = runs.get(sessionId); | ||
| if (!run || (scopeKey != null && run.scopeKey !== scopeKey)) return; | ||
| run.phase = "completed"; | ||
| run.itemId = null; | ||
| markRunUpdated(run); | ||
| recentRuns.set(sessionId, { ...run }); | ||
|
Comment on lines
+1955
to
+1958
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a tracked CLI is settled while its PTY remains active, this records AGENTS.md reference: AGENTS.md:L35-L35 Useful? React with 👍 / 👎. |
||
| runs.delete(sessionId); | ||
|
Comment on lines
+1958
to
+1959
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a tracked CLI is settled while its PTY remains live, deleting it from Useful? React with 👍 / 👎. |
||
| pendingAlerts = pendingAlerts.filter( | ||
| (alert) => | ||
| alert.dedupeKey !== `alert:${sessionId}:approval` | ||
| && alert.dedupeKey !== `alert:${sessionId}:question`, | ||
| ); | ||
| clearAlertDedupe(`alert:${sessionId}:approval`); | ||
| clearAlertDedupe(`alert:${sessionId}:question`); | ||
| scheduleFlush(true); | ||
| }, | ||
|
|
||
| /** Force a flush soon (e.g. right after a device registers). */ | ||
| poke(): void { | ||
| scheduleFlush(true); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a tracked CLI is already idle and the user settles it,
sessionService.get()derivesruntimeStatesolely from the persistedstatus, so a live session is returned asstatus: "running", runtimeState: "running"and this condition skipshandleSessionSettled. The earlier idle signal has already fired, and the supposed heartbeat is only evaluated while processing later output chunks, so a genuinely quiet CLI can leave its stale Live Activity open until the two-hour TTL. Fresh evidence beyond the prior settlement threads is that the centralized listener reads the non-projected SessionService row rather thanptyService's live idle state; resolve the live/projected runtime state here before deciding whether settlement is at rest.AGENTS.md reference: AGENTS.md:L35-L35
Useful? React with 👍 / 👎.