(
workspaceId: string,
observedRecency: number | null = null,
streaming = false
)
| 223 | } |
| 224 | |
| 225 | private async runForWorkspace( |
| 226 | workspaceId: string, |
| 227 | observedRecency: number | null = null, |
| 228 | streaming = false |
| 229 | ): Promise<void> { |
| 230 | try { |
| 231 | const transcript = await this.buildTrailingTranscript(workspaceId); |
| 232 | // Two hashes, two purposes: |
| 233 | // |
| 234 | // transcriptHash — keyed only on transcript bytes. Used by the |
| 235 | // history-catch-up guard (`isRecentRecencyAheadOfHistory` + |
| 236 | // `lastSeenInputHash`) to detect "transcript unchanged since the |
| 237 | // last look." Folding `streaming` into this comparison would make |
| 238 | // the common idle→streaming transition look like a transcript |
| 239 | // change and bypass the wait-for-history guard, letting the |
| 240 | // service persist a stale pre-pivot status and consume the |
| 241 | // recency signal. |
| 242 | // |
| 243 | // dedupHash — keyed on transcript + streaming. Used by the |
| 244 | // "settled, skip regeneration" branch (`state.lastInputHash`) |
| 245 | // because `streaming` now changes the prompt's tense guidance |
| 246 | // and therefore the generated status; identical transcript bytes |
| 247 | // with different streaming values must dedup independently. |
| 248 | const transcriptHash = computeTranscriptHash(transcript); |
| 249 | const dedupHash = computeDedupHash(transcriptHash, streaming); |
| 250 | // dispatch() set lastRanAt to the tick start time before kicking us |
| 251 | // off, so the scheduler won't reconsider this workspace until the next |
| 252 | // interval boundary unless a newer user-recency timestamp indicates the |
| 253 | // chat pivoted again. |
| 254 | const state = this.ensureState(workspaceId); |
| 255 | |
| 256 | const markRecencyObserved = () => { |
| 257 | if (observedRecency !== null) { |
| 258 | state.lastObservedRecency = observedRecency; |
| 259 | } |
| 260 | }; |
| 261 | // Settle this transcript: consume observed recency AND advance the dedup |
| 262 | // hash so the next tick won't regenerate against the same input. Used by |
| 263 | // the three branches that produce a definitive outcome for this transcript |
| 264 | // (post-provider failure, placeholder rejection, successful persist). |
| 265 | // Pre-provider failures and the empty/dedup-hit branches use bare |
| 266 | // `markRecencyObserved()` because they should still retry on the same |
| 267 | // transcript when conditions change. |
| 268 | const settleOnTranscript = () => { |
| 269 | markRecencyObserved(); |
| 270 | state.lastInputHash = dedupHash; |
| 271 | resetProviderFailureTracking(state); |
| 272 | }; |
| 273 | |
| 274 | if ( |
| 275 | isRecentRecencyAheadOfHistory( |
| 276 | state, |
| 277 | transcriptHash, |
| 278 | observedRecency, |
| 279 | this.clock(), |
| 280 | AGENT_STATUS_TICK_INTERVAL_MS |
| 281 | ) |
| 282 | ) { |
no test coverage detected