yzcode-cli 1.0.1 → 1.0.3

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.
Files changed (117) hide show
  1. package/assistant/sessionHistory.ts +87 -0
  2. package/bootstrap/state.ts +1769 -0
  3. package/bridge/bridgeApi.ts +539 -0
  4. package/bridge/bridgeConfig.ts +48 -0
  5. package/bridge/bridgeDebug.ts +135 -0
  6. package/bridge/bridgeEnabled.ts +202 -0
  7. package/bridge/bridgeMain.ts +2999 -0
  8. package/bridge/bridgeMessaging.ts +461 -0
  9. package/bridge/bridgePermissionCallbacks.ts +43 -0
  10. package/bridge/bridgePointer.ts +210 -0
  11. package/bridge/bridgeStatusUtil.ts +163 -0
  12. package/bridge/bridgeUI.ts +530 -0
  13. package/bridge/capacityWake.ts +56 -0
  14. package/bridge/codeSessionApi.ts +168 -0
  15. package/bridge/createSession.ts +384 -0
  16. package/bridge/debugUtils.ts +141 -0
  17. package/bridge/envLessBridgeConfig.ts +165 -0
  18. package/bridge/flushGate.ts +71 -0
  19. package/bridge/inboundAttachments.ts +175 -0
  20. package/bridge/inboundMessages.ts +80 -0
  21. package/bridge/initReplBridge.ts +569 -0
  22. package/bridge/jwtUtils.ts +256 -0
  23. package/bridge/pollConfig.ts +110 -0
  24. package/bridge/pollConfigDefaults.ts +82 -0
  25. package/bridge/remoteBridgeCore.ts +1008 -0
  26. package/bridge/replBridge.ts +2406 -0
  27. package/bridge/replBridgeHandle.ts +36 -0
  28. package/bridge/replBridgeTransport.ts +370 -0
  29. package/bridge/sessionIdCompat.ts +57 -0
  30. package/bridge/sessionRunner.ts +550 -0
  31. package/bridge/trustedDevice.ts +210 -0
  32. package/bridge/types.ts +262 -0
  33. package/bridge/workSecret.ts +127 -0
  34. package/buddy/CompanionSprite.tsx +371 -0
  35. package/buddy/companion.ts +133 -0
  36. package/buddy/prompt.ts +36 -0
  37. package/buddy/sprites.ts +514 -0
  38. package/buddy/types.ts +148 -0
  39. package/buddy/useBuddyNotification.tsx +98 -0
  40. package/coordinator/coordinatorMode.ts +369 -0
  41. package/memdir/findRelevantMemories.ts +141 -0
  42. package/memdir/memdir.ts +507 -0
  43. package/memdir/memoryAge.ts +53 -0
  44. package/memdir/memoryScan.ts +94 -0
  45. package/memdir/memoryTypes.ts +271 -0
  46. package/memdir/paths.ts +278 -0
  47. package/memdir/teamMemPaths.ts +292 -0
  48. package/memdir/teamMemPrompts.ts +100 -0
  49. package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
  50. package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
  51. package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
  52. package/migrations/migrateFennecToOpus.ts +45 -0
  53. package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
  54. package/migrations/migrateOpusToOpus1m.ts +43 -0
  55. package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
  56. package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
  57. package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
  58. package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
  59. package/migrations/resetProToOpusDefault.ts +51 -0
  60. package/native-ts/color-diff/index.ts +999 -0
  61. package/native-ts/file-index/index.ts +370 -0
  62. package/native-ts/yoga-layout/enums.ts +134 -0
  63. package/native-ts/yoga-layout/index.ts +2578 -0
  64. package/outputStyles/loadOutputStylesDir.ts +98 -0
  65. package/package.json +22 -5
  66. package/plugins/builtinPlugins.ts +159 -0
  67. package/plugins/bundled/index.ts +23 -0
  68. package/schemas/hooks.ts +222 -0
  69. package/screens/Doctor.tsx +575 -0
  70. package/screens/REPL.tsx +5006 -0
  71. package/screens/ResumeConversation.tsx +399 -0
  72. package/server/createDirectConnectSession.ts +88 -0
  73. package/server/directConnectManager.ts +213 -0
  74. package/server/types.ts +57 -0
  75. package/skills/bundled/batch.ts +124 -0
  76. package/skills/bundled/claudeApi.ts +196 -0
  77. package/skills/bundled/claudeApiContent.ts +75 -0
  78. package/skills/bundled/claudeInChrome.ts +34 -0
  79. package/skills/bundled/debug.ts +103 -0
  80. package/skills/bundled/index.ts +79 -0
  81. package/skills/bundled/keybindings.ts +339 -0
  82. package/skills/bundled/loop.ts +92 -0
  83. package/skills/bundled/loremIpsum.ts +282 -0
  84. package/skills/bundled/remember.ts +82 -0
  85. package/skills/bundled/scheduleRemoteAgents.ts +447 -0
  86. package/skills/bundled/simplify.ts +69 -0
  87. package/skills/bundled/skillify.ts +197 -0
  88. package/skills/bundled/stuck.ts +79 -0
  89. package/skills/bundled/updateConfig.ts +475 -0
  90. package/skills/bundled/verify/SKILL.md +3 -0
  91. package/skills/bundled/verify/examples/cli.md +3 -0
  92. package/skills/bundled/verify/examples/server.md +3 -0
  93. package/skills/bundled/verify.ts +30 -0
  94. package/skills/bundled/verifyContent.ts +13 -0
  95. package/skills/bundledSkills.ts +220 -0
  96. package/skills/loadSkillsDir.ts +1086 -0
  97. package/skills/mcpSkillBuilders.ts +44 -0
  98. package/tasks/DreamTask/DreamTask.ts +157 -0
  99. package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
  100. package/tasks/InProcessTeammateTask/types.ts +121 -0
  101. package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
  102. package/tasks/LocalMainSessionTask.ts +479 -0
  103. package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
  104. package/tasks/LocalShellTask/guards.ts +41 -0
  105. package/tasks/LocalShellTask/killShellTasks.ts +76 -0
  106. package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
  107. package/tasks/pillLabel.ts +82 -0
  108. package/tasks/stopTask.ts +100 -0
  109. package/tasks/types.ts +46 -0
  110. package/upstreamproxy/relay.ts +455 -0
  111. package/upstreamproxy/upstreamproxy.ts +285 -0
  112. package/vim/motions.ts +82 -0
  113. package/vim/operators.ts +556 -0
  114. package/vim/textObjects.ts +186 -0
  115. package/vim/transitions.ts +490 -0
  116. package/vim/types.ts +199 -0
  117. package/voice/voiceModeEnabled.ts +54 -0
@@ -0,0 +1,256 @@
1
+ import { logEvent } from '../services/analytics/index.js'
2
+ import { logForDebugging } from '../utils/debug.js'
3
+ import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
4
+ import { errorMessage } from '../utils/errors.js'
5
+ import { jsonParse } from '../utils/slowOperations.js'
6
+
7
+ /** Format a millisecond duration as a human-readable string (e.g. "5m 30s"). */
8
+ function formatDuration(ms: number): string {
9
+ if (ms < 60_000) return `${Math.round(ms / 1000)}s`
10
+ const m = Math.floor(ms / 60_000)
11
+ const s = Math.round((ms % 60_000) / 1000)
12
+ return s > 0 ? `${m}m ${s}s` : `${m}m`
13
+ }
14
+
15
+ /**
16
+ * Decode a JWT's payload segment without verifying the signature.
17
+ * Strips the `sk-ant-si-` session-ingress prefix if present.
18
+ * Returns the parsed JSON payload as `unknown`, or `null` if the
19
+ * token is malformed or the payload is not valid JSON.
20
+ */
21
+ export function decodeJwtPayload(token: string): unknown | null {
22
+ const jwt = token.startsWith('sk-ant-si-')
23
+ ? token.slice('sk-ant-si-'.length)
24
+ : token
25
+ const parts = jwt.split('.')
26
+ if (parts.length !== 3 || !parts[1]) return null
27
+ try {
28
+ return jsonParse(Buffer.from(parts[1], 'base64url').toString('utf8'))
29
+ } catch {
30
+ return null
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Decode the `exp` (expiry) claim from a JWT without verifying the signature.
36
+ * @returns The `exp` value in Unix seconds, or `null` if unparseable
37
+ */
38
+ export function decodeJwtExpiry(token: string): number | null {
39
+ const payload = decodeJwtPayload(token)
40
+ if (
41
+ payload !== null &&
42
+ typeof payload === 'object' &&
43
+ 'exp' in payload &&
44
+ typeof payload.exp === 'number'
45
+ ) {
46
+ return payload.exp
47
+ }
48
+ return null
49
+ }
50
+
51
+ /** Refresh buffer: request a new token before expiry. */
52
+ const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000
53
+
54
+ /** Fallback refresh interval when the new token's expiry is unknown. */
55
+ const FALLBACK_REFRESH_INTERVAL_MS = 30 * 60 * 1000 // 30 minutes
56
+
57
+ /** Max consecutive failures before giving up on the refresh chain. */
58
+ const MAX_REFRESH_FAILURES = 3
59
+
60
+ /** Retry delay when getAccessToken returns undefined. */
61
+ const REFRESH_RETRY_DELAY_MS = 60_000
62
+
63
+ /**
64
+ * Creates a token refresh scheduler that proactively refreshes session tokens
65
+ * before they expire. Used by both the standalone bridge and the REPL bridge.
66
+ *
67
+ * When a token is about to expire, the scheduler calls `onRefresh` with the
68
+ * session ID and the bridge's OAuth access token. The caller is responsible
69
+ * for delivering the token to the appropriate transport (child process stdin
70
+ * for standalone bridge, WebSocket reconnect for REPL bridge).
71
+ */
72
+ export function createTokenRefreshScheduler({
73
+ getAccessToken,
74
+ onRefresh,
75
+ label,
76
+ refreshBufferMs = TOKEN_REFRESH_BUFFER_MS,
77
+ }: {
78
+ getAccessToken: () => string | undefined | Promise<string | undefined>
79
+ onRefresh: (sessionId: string, oauthToken: string) => void
80
+ label: string
81
+ /** How long before expiry to fire refresh. Defaults to 5 min. */
82
+ refreshBufferMs?: number
83
+ }): {
84
+ schedule: (sessionId: string, token: string) => void
85
+ scheduleFromExpiresIn: (sessionId: string, expiresInSeconds: number) => void
86
+ cancel: (sessionId: string) => void
87
+ cancelAll: () => void
88
+ } {
89
+ const timers = new Map<string, ReturnType<typeof setTimeout>>()
90
+ const failureCounts = new Map<string, number>()
91
+ // Generation counter per session — incremented by schedule() and cancel()
92
+ // so that in-flight async doRefresh() calls can detect when they've been
93
+ // superseded and should skip setting follow-up timers.
94
+ const generations = new Map<string, number>()
95
+
96
+ function nextGeneration(sessionId: string): number {
97
+ const gen = (generations.get(sessionId) ?? 0) + 1
98
+ generations.set(sessionId, gen)
99
+ return gen
100
+ }
101
+
102
+ function schedule(sessionId: string, token: string): void {
103
+ const expiry = decodeJwtExpiry(token)
104
+ if (!expiry) {
105
+ // Token is not a decodable JWT (e.g. an OAuth token passed from the
106
+ // REPL bridge WebSocket open handler). Preserve any existing timer
107
+ // (such as the follow-up refresh set by doRefresh) so the refresh
108
+ // chain is not broken.
109
+ logForDebugging(
110
+ `[${label}:token] Could not decode JWT expiry for sessionId=${sessionId}, token prefix=${token.slice(0, 15)}…, keeping existing timer`,
111
+ )
112
+ return
113
+ }
114
+
115
+ // Clear any existing refresh timer — we have a concrete expiry to replace it.
116
+ const existing = timers.get(sessionId)
117
+ if (existing) {
118
+ clearTimeout(existing)
119
+ }
120
+
121
+ // Bump generation to invalidate any in-flight async doRefresh.
122
+ const gen = nextGeneration(sessionId)
123
+
124
+ const expiryDate = new Date(expiry * 1000).toISOString()
125
+ const delayMs = expiry * 1000 - Date.now() - refreshBufferMs
126
+ if (delayMs <= 0) {
127
+ logForDebugging(
128
+ `[${label}:token] Token for sessionId=${sessionId} expires=${expiryDate} (past or within buffer), refreshing immediately`,
129
+ )
130
+ void doRefresh(sessionId, gen)
131
+ return
132
+ }
133
+
134
+ logForDebugging(
135
+ `[${label}:token] Scheduled token refresh for sessionId=${sessionId} in ${formatDuration(delayMs)} (expires=${expiryDate}, buffer=${refreshBufferMs / 1000}s)`,
136
+ )
137
+
138
+ const timer = setTimeout(doRefresh, delayMs, sessionId, gen)
139
+ timers.set(sessionId, timer)
140
+ }
141
+
142
+ /**
143
+ * Schedule refresh using an explicit TTL (seconds until expiry) rather
144
+ * than decoding a JWT's exp claim. Used by callers whose JWT is opaque
145
+ * (e.g. POST /v1/code/sessions/{id}/bridge returns expires_in directly).
146
+ */
147
+ function scheduleFromExpiresIn(
148
+ sessionId: string,
149
+ expiresInSeconds: number,
150
+ ): void {
151
+ const existing = timers.get(sessionId)
152
+ if (existing) clearTimeout(existing)
153
+ const gen = nextGeneration(sessionId)
154
+ // Clamp to 30s floor — if refreshBufferMs exceeds the server's expires_in
155
+ // (e.g. very large buffer for frequent-refresh testing, or server shortens
156
+ // expires_in unexpectedly), unclamped delayMs ≤ 0 would tight-loop.
157
+ const delayMs = Math.max(expiresInSeconds * 1000 - refreshBufferMs, 30_000)
158
+ logForDebugging(
159
+ `[${label}:token] Scheduled token refresh for sessionId=${sessionId} in ${formatDuration(delayMs)} (expires_in=${expiresInSeconds}s, buffer=${refreshBufferMs / 1000}s)`,
160
+ )
161
+ const timer = setTimeout(doRefresh, delayMs, sessionId, gen)
162
+ timers.set(sessionId, timer)
163
+ }
164
+
165
+ async function doRefresh(sessionId: string, gen: number): Promise<void> {
166
+ let oauthToken: string | undefined
167
+ try {
168
+ oauthToken = await getAccessToken()
169
+ } catch (err) {
170
+ logForDebugging(
171
+ `[${label}:token] getAccessToken threw for sessionId=${sessionId}: ${errorMessage(err)}`,
172
+ { level: 'error' },
173
+ )
174
+ }
175
+
176
+ // If the session was cancelled or rescheduled while we were awaiting,
177
+ // the generation will have changed — bail out to avoid orphaned timers.
178
+ if (generations.get(sessionId) !== gen) {
179
+ logForDebugging(
180
+ `[${label}:token] doRefresh for sessionId=${sessionId} stale (gen ${gen} vs ${generations.get(sessionId)}), skipping`,
181
+ )
182
+ return
183
+ }
184
+
185
+ if (!oauthToken) {
186
+ const failures = (failureCounts.get(sessionId) ?? 0) + 1
187
+ failureCounts.set(sessionId, failures)
188
+ logForDebugging(
189
+ `[${label}:token] No OAuth token available for refresh, sessionId=${sessionId} (failure ${failures}/${MAX_REFRESH_FAILURES})`,
190
+ { level: 'error' },
191
+ )
192
+ logForDiagnosticsNoPII('error', 'bridge_token_refresh_no_oauth')
193
+ // Schedule a retry so the refresh chain can recover if the token
194
+ // becomes available again (e.g. transient cache clear during refresh).
195
+ // Cap retries to avoid spamming on genuine failures.
196
+ if (failures < MAX_REFRESH_FAILURES) {
197
+ const retryTimer = setTimeout(
198
+ doRefresh,
199
+ REFRESH_RETRY_DELAY_MS,
200
+ sessionId,
201
+ gen,
202
+ )
203
+ timers.set(sessionId, retryTimer)
204
+ }
205
+ return
206
+ }
207
+
208
+ // Reset failure counter on successful token retrieval
209
+ failureCounts.delete(sessionId)
210
+
211
+ logForDebugging(
212
+ `[${label}:token] Refreshing token for sessionId=${sessionId}: new token prefix=${oauthToken.slice(0, 15)}…`,
213
+ )
214
+ logEvent('tengu_bridge_token_refreshed', {})
215
+ onRefresh(sessionId, oauthToken)
216
+
217
+ // Schedule a follow-up refresh so long-running sessions stay authenticated.
218
+ // Without this, the initial one-shot timer leaves the session vulnerable
219
+ // to token expiry if it runs past the first refresh window.
220
+ const timer = setTimeout(
221
+ doRefresh,
222
+ FALLBACK_REFRESH_INTERVAL_MS,
223
+ sessionId,
224
+ gen,
225
+ )
226
+ timers.set(sessionId, timer)
227
+ logForDebugging(
228
+ `[${label}:token] Scheduled follow-up refresh for sessionId=${sessionId} in ${formatDuration(FALLBACK_REFRESH_INTERVAL_MS)}`,
229
+ )
230
+ }
231
+
232
+ function cancel(sessionId: string): void {
233
+ // Bump generation to invalidate any in-flight async doRefresh.
234
+ nextGeneration(sessionId)
235
+ const timer = timers.get(sessionId)
236
+ if (timer) {
237
+ clearTimeout(timer)
238
+ timers.delete(sessionId)
239
+ }
240
+ failureCounts.delete(sessionId)
241
+ }
242
+
243
+ function cancelAll(): void {
244
+ // Bump all generations so in-flight doRefresh calls are invalidated.
245
+ for (const sessionId of generations.keys()) {
246
+ nextGeneration(sessionId)
247
+ }
248
+ for (const timer of timers.values()) {
249
+ clearTimeout(timer)
250
+ }
251
+ timers.clear()
252
+ failureCounts.clear()
253
+ }
254
+
255
+ return { schedule, scheduleFromExpiresIn, cancel, cancelAll }
256
+ }
@@ -0,0 +1,110 @@
1
+ import { z } from 'zod/v4'
2
+ import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics/growthbook.js'
3
+ import { lazySchema } from '../utils/lazySchema.js'
4
+ import {
5
+ DEFAULT_POLL_CONFIG,
6
+ type PollIntervalConfig,
7
+ } from './pollConfigDefaults.js'
8
+
9
+ // .min(100) on the seek-work intervals restores the old Math.max(..., 100)
10
+ // defense-in-depth floor against fat-fingered GrowthBook values. Unlike a
11
+ // clamp, Zod rejects the whole object on violation — a config with one bad
12
+ // field falls back to DEFAULT_POLL_CONFIG entirely rather than being
13
+ // partially trusted.
14
+ //
15
+ // The at_capacity intervals use a 0-or-≥100 refinement: 0 means "disabled"
16
+ // (heartbeat-only mode), ≥100 is the fat-finger floor. Values 1–99 are
17
+ // rejected so unit confusion (ops thinks seconds, enters 10) doesn't poll
18
+ // every 10ms against the VerifyEnvironmentSecretAuth DB path.
19
+ //
20
+ // The object-level refines require at least one at-capacity liveness
21
+ // mechanism enabled: heartbeat OR the relevant poll interval. Without this,
22
+ // the hb=0, atCapMs=0 drift config (ops disables heartbeat without
23
+ // restoring at_capacity) falls through every throttle site with no sleep —
24
+ // tight-looping /poll at HTTP-round-trip speed.
25
+ const zeroOrAtLeast100 = {
26
+ message: 'must be 0 (disabled) or ≥100ms',
27
+ }
28
+ const pollIntervalConfigSchema = lazySchema(() =>
29
+ z
30
+ .object({
31
+ poll_interval_ms_not_at_capacity: z.number().int().min(100),
32
+ // 0 = no at-capacity polling. Independent of heartbeat — both can be
33
+ // enabled (heartbeat runs, periodically breaks out to poll).
34
+ poll_interval_ms_at_capacity: z
35
+ .number()
36
+ .int()
37
+ .refine(v => v === 0 || v >= 100, zeroOrAtLeast100),
38
+ // 0 = disabled; positive value = heartbeat at this interval while at
39
+ // capacity. Runs alongside at-capacity polling, not instead of it.
40
+ // Named non_exclusive to distinguish from the old heartbeat_interval_ms
41
+ // (either-or semantics in pre-#22145 clients). .default(0) so existing
42
+ // GrowthBook configs without this field parse successfully.
43
+ non_exclusive_heartbeat_interval_ms: z.number().int().min(0).default(0),
44
+ // Multisession (bridgeMain.ts) intervals. Defaults match the
45
+ // single-session values so existing configs without these fields
46
+ // preserve current behavior.
47
+ multisession_poll_interval_ms_not_at_capacity: z
48
+ .number()
49
+ .int()
50
+ .min(100)
51
+ .default(
52
+ DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_not_at_capacity,
53
+ ),
54
+ multisession_poll_interval_ms_partial_capacity: z
55
+ .number()
56
+ .int()
57
+ .min(100)
58
+ .default(
59
+ DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_partial_capacity,
60
+ ),
61
+ multisession_poll_interval_ms_at_capacity: z
62
+ .number()
63
+ .int()
64
+ .refine(v => v === 0 || v >= 100, zeroOrAtLeast100)
65
+ .default(DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_at_capacity),
66
+ // .min(1) matches the server's ge=1 constraint (work_v1.py:230).
67
+ reclaim_older_than_ms: z.number().int().min(1).default(5000),
68
+ session_keepalive_interval_v2_ms: z
69
+ .number()
70
+ .int()
71
+ .min(0)
72
+ .default(120_000),
73
+ })
74
+ .refine(
75
+ cfg =>
76
+ cfg.non_exclusive_heartbeat_interval_ms > 0 ||
77
+ cfg.poll_interval_ms_at_capacity > 0,
78
+ {
79
+ message:
80
+ 'at-capacity liveness requires non_exclusive_heartbeat_interval_ms > 0 or poll_interval_ms_at_capacity > 0',
81
+ },
82
+ )
83
+ .refine(
84
+ cfg =>
85
+ cfg.non_exclusive_heartbeat_interval_ms > 0 ||
86
+ cfg.multisession_poll_interval_ms_at_capacity > 0,
87
+ {
88
+ message:
89
+ 'at-capacity liveness requires non_exclusive_heartbeat_interval_ms > 0 or multisession_poll_interval_ms_at_capacity > 0',
90
+ },
91
+ ),
92
+ )
93
+
94
+ /**
95
+ * Fetch the bridge poll interval config from GrowthBook with a 5-minute
96
+ * refresh window. Validates the served JSON against the schema; falls back
97
+ * to defaults if the flag is absent, malformed, or partially-specified.
98
+ *
99
+ * Shared by bridgeMain.ts (standalone) and replBridge.ts (REPL) so ops
100
+ * can tune both poll rates fleet-wide with a single config push.
101
+ */
102
+ export function getPollIntervalConfig(): PollIntervalConfig {
103
+ const raw = getFeatureValue_CACHED_WITH_REFRESH<unknown>(
104
+ 'tengu_bridge_poll_interval_config',
105
+ DEFAULT_POLL_CONFIG,
106
+ 5 * 60 * 1000,
107
+ )
108
+ const parsed = pollIntervalConfigSchema().safeParse(raw)
109
+ return parsed.success ? parsed.data : DEFAULT_POLL_CONFIG
110
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Bridge poll interval defaults. Extracted from pollConfig.ts so callers
3
+ * that don't need live GrowthBook tuning (daemon via Agent SDK) can avoid
4
+ * the growthbook.ts → config.ts → file.ts → sessionStorage.ts → commands.ts
5
+ * transitive dependency chain.
6
+ */
7
+
8
+ /**
9
+ * Poll interval when actively seeking work (no transport / below maxSessions).
10
+ * Governs user-visible "connecting…" latency on initial work pickup and
11
+ * recovery speed after the server re-dispatches a work item.
12
+ */
13
+ const POLL_INTERVAL_MS_NOT_AT_CAPACITY = 2000
14
+
15
+ /**
16
+ * Poll interval when the transport is connected. Runs independently of
17
+ * heartbeat — when both are enabled, the heartbeat loop breaks out to poll
18
+ * at this interval. Set to 0 to disable at-capacity polling entirely.
19
+ *
20
+ * Server-side constraints that bound this value:
21
+ * - BRIDGE_LAST_POLL_TTL = 4h (Redis key expiry → environment auto-archived)
22
+ * - max_poll_stale_seconds = 24h (session-creation health gate, currently disabled)
23
+ *
24
+ * 10 minutes gives 24× headroom on the Redis TTL while still picking up
25
+ * server-initiated token-rotation redispatches within one poll cycle.
26
+ * The transport auto-reconnects internally for 10 minutes on transient WS
27
+ * failures, so poll is not the recovery path — it's strictly a liveness
28
+ * signal plus a backstop for permanent close.
29
+ */
30
+ const POLL_INTERVAL_MS_AT_CAPACITY = 600_000
31
+
32
+ /**
33
+ * Multisession bridge (bridgeMain.ts) poll intervals. Defaults match the
34
+ * single-session values so existing GrowthBook configs without these fields
35
+ * preserve current behavior. Ops can tune these independently via the
36
+ * tengu_bridge_poll_interval_config GB flag.
37
+ */
38
+ const MULTISESSION_POLL_INTERVAL_MS_NOT_AT_CAPACITY =
39
+ POLL_INTERVAL_MS_NOT_AT_CAPACITY
40
+ const MULTISESSION_POLL_INTERVAL_MS_PARTIAL_CAPACITY =
41
+ POLL_INTERVAL_MS_NOT_AT_CAPACITY
42
+ const MULTISESSION_POLL_INTERVAL_MS_AT_CAPACITY = POLL_INTERVAL_MS_AT_CAPACITY
43
+
44
+ export type PollIntervalConfig = {
45
+ poll_interval_ms_not_at_capacity: number
46
+ poll_interval_ms_at_capacity: number
47
+ non_exclusive_heartbeat_interval_ms: number
48
+ multisession_poll_interval_ms_not_at_capacity: number
49
+ multisession_poll_interval_ms_partial_capacity: number
50
+ multisession_poll_interval_ms_at_capacity: number
51
+ reclaim_older_than_ms: number
52
+ session_keepalive_interval_v2_ms: number
53
+ }
54
+
55
+ export const DEFAULT_POLL_CONFIG: PollIntervalConfig = {
56
+ poll_interval_ms_not_at_capacity: POLL_INTERVAL_MS_NOT_AT_CAPACITY,
57
+ poll_interval_ms_at_capacity: POLL_INTERVAL_MS_AT_CAPACITY,
58
+ // 0 = disabled. When > 0, at-capacity loops send per-work-item heartbeats
59
+ // at this interval. Independent of poll_interval_ms_at_capacity — both may
60
+ // run (heartbeat periodically yields to poll). 60s gives 5× headroom under
61
+ // the server's 300s heartbeat TTL. Named non_exclusive to distinguish from
62
+ // the old heartbeat_interval_ms field (either-or semantics in pre-#22145
63
+ // clients — heartbeat suppressed poll). Old clients ignore this key; ops
64
+ // can set both fields during rollout.
65
+ non_exclusive_heartbeat_interval_ms: 0,
66
+ multisession_poll_interval_ms_not_at_capacity:
67
+ MULTISESSION_POLL_INTERVAL_MS_NOT_AT_CAPACITY,
68
+ multisession_poll_interval_ms_partial_capacity:
69
+ MULTISESSION_POLL_INTERVAL_MS_PARTIAL_CAPACITY,
70
+ multisession_poll_interval_ms_at_capacity:
71
+ MULTISESSION_POLL_INTERVAL_MS_AT_CAPACITY,
72
+ // Poll query param: reclaim unacknowledged work items older than this.
73
+ // Matches the server's DEFAULT_RECLAIM_OLDER_THAN_MS (work_service.py:24).
74
+ // Enables picking up stale-pending work after JWT expiry, when the prior
75
+ // ack failed because the session_ingress_token was already stale.
76
+ reclaim_older_than_ms: 5000,
77
+ // 0 = disabled. When > 0, push a silent {type:'keep_alive'} frame to
78
+ // session-ingress at this interval so upstream proxies don't GC an idle
79
+ // remote-control session. 2 min is the default. _v2: bridge-only gate
80
+ // (pre-v2 clients read the old key, new clients ignore it).
81
+ session_keepalive_interval_v2_ms: 120_000,
82
+ }