work-ally 0.2.0-alpha.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.
Files changed (172) hide show
  1. package/AGENTS.md +110 -0
  2. package/DASHBOARD.md +160 -0
  3. package/PRODUCT.md +113 -0
  4. package/README.md +403 -0
  5. package/ally.sh +171 -0
  6. package/bridge/src/approval-rules.ts +360 -0
  7. package/bridge/src/channel-delivery.ts +207 -0
  8. package/bridge/src/channel-types.ts +22 -0
  9. package/bridge/src/channels/fake/adapter.ts +31 -0
  10. package/bridge/src/channels/feishu/adapter.ts +411 -0
  11. package/bridge/src/channels/feishu/approvals.ts +6 -0
  12. package/bridge/src/channels/feishu/formatter.ts +276 -0
  13. package/bridge/src/channels/feishu/normalize.ts +368 -0
  14. package/bridge/src/codex-config.ts +52 -0
  15. package/bridge/src/config.ts +240 -0
  16. package/bridge/src/fake-runtime-client.ts +505 -0
  17. package/bridge/src/handoff-service.ts +494 -0
  18. package/bridge/src/logger.ts +194 -0
  19. package/bridge/src/memory-digest.ts +186 -0
  20. package/bridge/src/receiver-approval-autonomy.ts +158 -0
  21. package/bridge/src/receiver-control-core.ts +140 -0
  22. package/bridge/src/receiver-control-work-session.ts +218 -0
  23. package/bridge/src/receiver-control.ts +83 -0
  24. package/bridge/src/receiver-delivery.ts +136 -0
  25. package/bridge/src/receiver-helpers.ts +96 -0
  26. package/bridge/src/receiver-human-gate.ts +333 -0
  27. package/bridge/src/receiver-inbound-preflight.ts +162 -0
  28. package/bridge/src/receiver-recovery.ts +236 -0
  29. package/bridge/src/receiver-runtime-callbacks.ts +367 -0
  30. package/bridge/src/receiver-runtime-policy.ts +132 -0
  31. package/bridge/src/receiver-runtime-state.ts +124 -0
  32. package/bridge/src/receiver-support-actions.ts +189 -0
  33. package/bridge/src/receiver-thread-start.ts +57 -0
  34. package/bridge/src/receiver-turn-coordination.ts +94 -0
  35. package/bridge/src/receiver-turn-execution.ts +257 -0
  36. package/bridge/src/receiver-turn-failure.ts +143 -0
  37. package/bridge/src/receiver-turn-result.ts +185 -0
  38. package/bridge/src/receiver-turn-steer.ts +70 -0
  39. package/bridge/src/receiver-work-session.ts +76 -0
  40. package/bridge/src/receiver.ts +329 -0
  41. package/bridge/src/router.ts +62 -0
  42. package/bridge/src/runtime-client-agent-messages.ts +150 -0
  43. package/bridge/src/runtime-client-message-dispatch.ts +176 -0
  44. package/bridge/src/runtime-client-protocol.ts +411 -0
  45. package/bridge/src/runtime-client-request-ops.ts +56 -0
  46. package/bridge/src/runtime-client-run-turn.ts +158 -0
  47. package/bridge/src/runtime-client-thread-ops.ts +270 -0
  48. package/bridge/src/runtime-client-transport.ts +309 -0
  49. package/bridge/src/runtime-client-turn-poll.ts +224 -0
  50. package/bridge/src/runtime-client-turn-read.ts +185 -0
  51. package/bridge/src/runtime-client-turn-state.ts +105 -0
  52. package/bridge/src/runtime-client.ts +344 -0
  53. package/bridge/src/runtime-user-input.ts +403 -0
  54. package/bridge/src/scheduler.ts +239 -0
  55. package/bridge/src/server-handoff-command.ts +364 -0
  56. package/bridge/src/server-main.ts +80 -0
  57. package/bridge/src/server-routine-command.ts +60 -0
  58. package/bridge/src/server-routine-execution.ts +222 -0
  59. package/bridge/src/server-runtime-app-support.ts +107 -0
  60. package/bridge/src/server-runtime-app.ts +238 -0
  61. package/bridge/src/server-thread-sync-command.ts +63 -0
  62. package/bridge/src/server.ts +17 -0
  63. package/bridge/src/session-store-delivery.ts +220 -0
  64. package/bridge/src/session-store-human-gate.ts +380 -0
  65. package/bridge/src/session-store-inbound-acceptance.ts +66 -0
  66. package/bridge/src/session-store-meta.ts +134 -0
  67. package/bridge/src/session-store-turn-ledger.ts +272 -0
  68. package/bridge/src/session-store.ts +380 -0
  69. package/bridge/src/system-notify.ts +220 -0
  70. package/bridge/src/thread-sync.ts +200 -0
  71. package/bridge/src/translator.ts +494 -0
  72. package/bridge/src/types.ts +289 -0
  73. package/bridge/src/utils.ts +104 -0
  74. package/bridge/src/work-session-store.ts +471 -0
  75. package/docs/.gitkeep +0 -0
  76. package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
  77. package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
  78. package/docs/completed/README.md +21 -0
  79. package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
  80. package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
  81. package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
  82. package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
  83. package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
  84. package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
  85. package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
  86. package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
  87. package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
  88. package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
  89. package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
  90. package/docs/developer-workflow.md +49 -0
  91. package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
  92. package/docs/implementation/test-coverage-map.md +363 -0
  93. package/docs/implementation/work-ally-implementation-guide.md +790 -0
  94. package/docs/issues/README.md +10 -0
  95. package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
  96. package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
  97. package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
  98. package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
  99. package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
  100. package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
  101. package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
  102. package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
  103. package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
  104. package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
  105. package/docs/manual-acceptance.md +127 -0
  106. package/docs/ops-runbook.md +44 -0
  107. package/docs/planning/FEATURE-memory-system.md +748 -0
  108. package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
  109. package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
  110. package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
  111. package/docs/planning/SPEC-assistant-rename.md +610 -0
  112. package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
  113. package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
  114. package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
  115. package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
  116. package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
  117. package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
  118. package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
  119. package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
  120. package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
  121. package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
  122. package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
  123. package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
  124. package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
  125. package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
  126. package/docs/planning/SPEC-stable-archive-contract.md +456 -0
  127. package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
  128. package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
  129. package/docs/planning/ally-next.md +1278 -0
  130. package/docs/planning/assistant-workbench-spec.md +725 -0
  131. package/docs/planning/product-workbench.md +283 -0
  132. package/docs/product-onboarding.md +227 -0
  133. package/docs/product-spec-standard.md +528 -0
  134. package/docs/troubleshooting.md +45 -0
  135. package/docs/user-quickstart.md +46 -0
  136. package/internal/dispatch.sh +95 -0
  137. package/internal/lib/common.sh +1450 -0
  138. package/internal/modules/assistant/manage.sh +1312 -0
  139. package/internal/modules/bootstrap/setup.sh +144 -0
  140. package/internal/modules/config/init-env.sh +10 -0
  141. package/internal/modules/global/manage.sh +154 -0
  142. package/internal/modules/handoff/manage.sh +54 -0
  143. package/internal/modules/mcp/manage.sh +83 -0
  144. package/internal/modules/ops/logs.sh +76 -0
  145. package/internal/modules/routines/manage.sh +55 -0
  146. package/internal/modules/runtime/assistant-autosave.sh +26 -0
  147. package/internal/modules/runtime/restart.sh +6 -0
  148. package/internal/modules/runtime/start.sh +283 -0
  149. package/internal/modules/runtime/status.sh +194 -0
  150. package/internal/modules/runtime/stop.sh +55 -0
  151. package/internal/modules/runtime/supervisor.sh +216 -0
  152. package/internal/modules/runtime/update.sh +26 -0
  153. package/package.json +41 -0
  154. package/runtime/config/.gitkeep +0 -0
  155. package/runtime/host/.gitkeep +0 -0
  156. package/runtime/host/healthcheck-codex-app-server.ts +22 -0
  157. package/runtime/host/ping-pong-codex-app-server.ts +66 -0
  158. package/runtime/host/probe-codex-app-server.ts +115 -0
  159. package/skills/archive-reader/SKILL.md +9 -0
  160. package/skills/feishu-production-debug/SKILL.md +37 -0
  161. package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
  162. package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
  163. package/skills/issue-to-spec-triage/SKILL.md +44 -0
  164. package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
  165. package/skills/memory-digest/SKILL.md +9 -0
  166. package/skills/post-implementation-closure/SKILL.md +39 -0
  167. package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
  168. package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
  169. package/skills/product-spec/SKILL.md +244 -0
  170. package/templates/env.example +5 -0
  171. package/templates/routines/nightly-memory-digest.yaml +10 -0
  172. package/templates/workspace/AGENTS.md +26 -0
@@ -0,0 +1,309 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createInterface } from 'node:readline';
3
+ import { logError, logInfo, logWarn } from './logger.ts';
4
+ import type { ConnectionLifecycleEvent } from './types.ts';
5
+
6
+ interface RuntimeClientTransportOptions {
7
+ codexHome: string | null;
8
+ codexBin: string;
9
+ spawnImpl: typeof spawn;
10
+ connectTimeoutMs: number;
11
+ disconnectTimeoutMs: number;
12
+ reconnectBaseDelayMs: number;
13
+ reconnectMaxDelayMs: number;
14
+ initialize(): Promise<void>;
15
+ onMessage(line: string): void;
16
+ rejectAllPending(error: Error): void;
17
+ rejectAllTurns(error: Error): void;
18
+ activeTurnCount(): number;
19
+ emitConnectionEvent(event: ConnectionLifecycleEvent): void;
20
+ }
21
+
22
+ function reconnectDelayForAttempt(attempt: number, baseMs: number, maxMs: number): number {
23
+ const base = Math.max(250, baseMs);
24
+ const max = Math.max(base, maxMs);
25
+ const delay = base * Math.pow(2, Math.max(0, attempt - 1));
26
+ return Math.min(max, delay);
27
+ }
28
+
29
+ export class RuntimeClientTransport {
30
+ private readonly options: RuntimeClientTransportOptions;
31
+ private child: ReturnType<typeof spawn> | null = null;
32
+ private stdoutInterface: ReturnType<typeof createInterface> | null = null;
33
+ private connectPromise: Promise<void> | null = null;
34
+ private reconnectTimer: NodeJS.Timeout | null = null;
35
+ private reconnectAttempt = 0;
36
+ private reconnectNotified = false;
37
+ private manualDisconnect = false;
38
+
39
+ constructor(options: RuntimeClientTransportOptions) {
40
+ this.options = options;
41
+ }
42
+
43
+ isReady(): boolean {
44
+ return Boolean(
45
+ this.child
46
+ && this.child.exitCode === null
47
+ && !this.child.killed
48
+ && this.child.stdin
49
+ && !this.child.stdin.destroyed
50
+ && !this.child.stdin.writableEnded,
51
+ );
52
+ }
53
+
54
+ sendRaw(payload: unknown): void {
55
+ if (!this.isReady() || !this.child?.stdin) {
56
+ throw new Error('runtime stdio is not connected');
57
+ }
58
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`);
59
+ }
60
+
61
+ scheduleReconnect(reason: string, reconnect: () => Promise<void>): void {
62
+ if (this.manualDisconnect || this.reconnectTimer) {
63
+ return;
64
+ }
65
+ const attempt = this.reconnectAttempt + 1;
66
+ const delayMs = reconnectDelayForAttempt(
67
+ attempt,
68
+ this.options.reconnectBaseDelayMs,
69
+ this.options.reconnectMaxDelayMs,
70
+ );
71
+ this.reconnectAttempt = attempt;
72
+ this.reconnectNotified = true;
73
+ logWarn('runtime', 'reconnect_scheduled', { transport: 'stdio', attempt, delayMs, reason });
74
+ if (attempt === 1) {
75
+ this.options.emitConnectionEvent({
76
+ source: 'runtime',
77
+ state: 'disconnected',
78
+ reason,
79
+ attempt,
80
+ nextRetryDelayMs: delayMs,
81
+ });
82
+ }
83
+ this.options.emitConnectionEvent({
84
+ source: 'runtime',
85
+ state: 'reconnecting',
86
+ reason,
87
+ attempt,
88
+ nextRetryDelayMs: delayMs,
89
+ });
90
+ this.reconnectTimer = setTimeout(() => {
91
+ this.reconnectTimer = null;
92
+ reconnect().catch((error) => {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ this.scheduleReconnect(message, reconnect);
95
+ });
96
+ }, delayMs);
97
+ this.reconnectTimer.unref?.();
98
+ }
99
+
100
+ handleTransportClosed(reason: string, scheduleReconnect: (reason: string) => void): void {
101
+ logWarn('runtime', 'transport_closed', { transport: 'stdio', reason });
102
+ this.clearTransportHandles();
103
+ this.connectPromise = null;
104
+ const closedError = new Error(reason);
105
+ this.options.rejectAllPending(closedError);
106
+ if (this.manualDisconnect) {
107
+ this.options.rejectAllTurns(closedError);
108
+ return;
109
+ }
110
+ const activeTurnCount = this.options.activeTurnCount();
111
+ if (activeTurnCount > 0) {
112
+ logWarn('runtime', 'turns_waiting_for_pull_reconcile_after_disconnect', {
113
+ activeTurnCount,
114
+ reason,
115
+ });
116
+ }
117
+ scheduleReconnect(reason);
118
+ }
119
+
120
+ async connect(): Promise<void> {
121
+ this.manualDisconnect = false;
122
+ if (this.isReady()) {
123
+ return;
124
+ }
125
+ if (this.connectPromise) {
126
+ return this.connectPromise;
127
+ }
128
+
129
+ this.connectPromise = new Promise<void>((resolve, reject) => {
130
+ logInfo('runtime', 'connect_start', { transport: 'stdio', codexBin: this.options.codexBin });
131
+ const child = this.options.spawnImpl(this.options.codexBin, ['app-server', '--listen', 'stdio://'], {
132
+ stdio: ['pipe', 'pipe', 'pipe'],
133
+ env: {
134
+ ...process.env,
135
+ ...(this.options.codexHome ? { CODEX_HOME: this.options.codexHome } : {}),
136
+ },
137
+ });
138
+ this.child = child;
139
+ this.stdoutInterface = createInterface({
140
+ input: child.stdout,
141
+ crlfDelay: Infinity,
142
+ });
143
+ this.stdoutInterface.on('line', (line) => {
144
+ if (!line) {
145
+ return;
146
+ }
147
+ try {
148
+ this.options.onMessage(line);
149
+ } catch (error) {
150
+ logWarn('runtime', 'stdout_line_parse_failed', {
151
+ error: error instanceof Error ? (error.stack || error.message) : String(error),
152
+ line,
153
+ });
154
+ }
155
+ });
156
+ child.stderr.on('data', (chunk) => {
157
+ const text = String(chunk).trim();
158
+ if (!text) {
159
+ return;
160
+ }
161
+ logWarn('runtime', 'child_stderr', { text });
162
+ });
163
+ child.stdin.on('error', (error) => {
164
+ logWarn('runtime', 'stdin_error', {
165
+ error: error instanceof Error ? (error.stack || error.message) : String(error),
166
+ });
167
+ });
168
+
169
+ let settled = false;
170
+ const timeout = setTimeout(() => {
171
+ if (settled) {
172
+ return;
173
+ }
174
+ settled = true;
175
+ try {
176
+ child.kill('SIGTERM');
177
+ } catch {
178
+ // noop
179
+ }
180
+ reject(new Error(`runtime connect timed out: stdio://${this.options.codexBin}`));
181
+ }, this.options.connectTimeoutMs);
182
+
183
+ const fail = (error: unknown) => {
184
+ if (settled) {
185
+ return;
186
+ }
187
+ settled = true;
188
+ clearTimeout(timeout);
189
+ reject(error instanceof Error ? error : new Error(String(error)));
190
+ };
191
+
192
+ child.once('spawn', async () => {
193
+ logInfo('runtime', 'connect_open', {
194
+ transport: 'stdio',
195
+ pid: child.pid || null,
196
+ });
197
+ this.clearReconnectTimer();
198
+ try {
199
+ await this.options.initialize();
200
+ if (this.reconnectNotified) {
201
+ this.options.emitConnectionEvent({
202
+ source: 'runtime',
203
+ state: 'reconnected',
204
+ attempt: this.reconnectAttempt || null,
205
+ reason: null,
206
+ nextRetryDelayMs: null,
207
+ });
208
+ }
209
+ this.reconnectAttempt = 0;
210
+ this.reconnectNotified = false;
211
+ if (!settled) {
212
+ settled = true;
213
+ clearTimeout(timeout);
214
+ resolve();
215
+ }
216
+ } catch (error) {
217
+ fail(error);
218
+ try {
219
+ child.kill('SIGTERM');
220
+ } catch {
221
+ // noop
222
+ }
223
+ }
224
+ });
225
+
226
+ child.once('error', (error) => {
227
+ logError('runtime', 'connect_error', {
228
+ transport: 'stdio',
229
+ error: error instanceof Error ? (error.stack || error.message) : String(error),
230
+ });
231
+ fail(error);
232
+ });
233
+
234
+ child.once('close', (code, signal) => {
235
+ const reason = `runtime child process exited${code !== null ? ` (code: ${code})` : ''}${signal ? ` (signal: ${signal})` : ''}`;
236
+ if (!settled) {
237
+ fail(new Error(reason));
238
+ }
239
+ this.handleTransportClosed(reason, (message) => this.scheduleReconnect(message, () => this.connect()));
240
+ });
241
+ });
242
+
243
+ try {
244
+ await this.connectPromise;
245
+ } catch (error) {
246
+ this.connectPromise = null;
247
+ this.clearTransportHandles();
248
+ throw error;
249
+ }
250
+ }
251
+
252
+ async disconnect(): Promise<void> {
253
+ this.manualDisconnect = true;
254
+ this.clearReconnectTimer();
255
+ const child = this.child;
256
+ this.connectPromise = null;
257
+ if (!child || child.exitCode !== null || child.killed) {
258
+ this.clearTransportHandles();
259
+ return;
260
+ }
261
+
262
+ await new Promise<void>((resolve) => {
263
+ let settled = false;
264
+ const done = () => {
265
+ if (settled) {
266
+ return;
267
+ }
268
+ settled = true;
269
+ resolve();
270
+ };
271
+ const timer = setTimeout(done, this.options.disconnectTimeoutMs);
272
+ child.once('close', () => {
273
+ clearTimeout(timer);
274
+ done();
275
+ });
276
+ try {
277
+ child.kill('SIGTERM');
278
+ } catch {
279
+ clearTimeout(timer);
280
+ done();
281
+ }
282
+ });
283
+
284
+ this.clearTransportHandles();
285
+ }
286
+
287
+ private clearReconnectTimer(): void {
288
+ if (!this.reconnectTimer) {
289
+ return;
290
+ }
291
+ clearTimeout(this.reconnectTimer);
292
+ this.reconnectTimer = null;
293
+ }
294
+
295
+ private clearTransportHandles(): void {
296
+ if (this.stdoutInterface) {
297
+ this.stdoutInterface.removeAllListeners();
298
+ this.stdoutInterface.close();
299
+ this.stdoutInterface = null;
300
+ }
301
+ if (this.child) {
302
+ this.child.removeAllListeners();
303
+ this.child.stdout?.removeAllListeners();
304
+ this.child.stderr?.removeAllListeners();
305
+ this.child.stdin?.removeAllListeners();
306
+ this.child = null;
307
+ }
308
+ }
309
+ }
@@ -0,0 +1,224 @@
1
+ import { buildReplyFromThreadItems, consumeCompletedAgentMessageProgress, noteAgentMessageItem, type RuntimeTurnState } from './runtime-client-agent-messages.ts';
2
+ import { enrichTerminalReply, mergeTerminalTurnResult, normalizeTurnError, readTurnSnapshot } from './runtime-client-turn-read.ts';
3
+ import { logInfo, logWarn } from './logger.ts';
4
+ import { normalizeRuntimeError } from './runtime-client-protocol.ts';
5
+ import type { RuntimeProgressMeta, RuntimeThreadStatus, RuntimeTurnResult } from './types.ts';
6
+
7
+ interface TurnFinalPollDeps {
8
+ turnStates: Map<string, RuntimeTurnState>;
9
+ request: (method: string, params: unknown) => Promise<any>;
10
+ cacheThreadStatus: (threadId: string, status: RuntimeThreadStatus | null | undefined) => RuntimeThreadStatus | null;
11
+ readPersistedFinalReply: (turnId: string) => string | null;
12
+ resolveTurnResult: (turnId: string, result: RuntimeTurnResult) => boolean;
13
+ rejectTurnResult: (turnId: string, error: Error) => boolean;
14
+ isRecoverableRuntimeError: (error: unknown) => boolean;
15
+ safeEmitTurnProgress: (turnId: string, text: string, meta?: RuntimeProgressMeta) => Promise<void>;
16
+ }
17
+
18
+ interface TurnFinalPollOptions {
19
+ pollMs: number;
20
+ transportTimeoutMs: number;
21
+ idleReconcileMs: number;
22
+ idleAwaitingAuthoritativeResultTimeoutMs: number;
23
+ }
24
+
25
+ function sleep(ms: number): Promise<void> {
26
+ return new Promise((resolve) => setTimeout(resolve, ms));
27
+ }
28
+
29
+ function isWaitingOnHumanStatus(status: RuntimeThreadStatus | null | undefined): boolean {
30
+ return status?.type === 'active' && status.activeFlags.some((flag) => flag === 'waitingOnApproval' || flag === 'waitingOnUserInput');
31
+ }
32
+
33
+ async function observePolledTurnItems(
34
+ turnStates: Map<string, RuntimeTurnState>,
35
+ turnId: string,
36
+ items: Array<any>,
37
+ safeEmitTurnProgress: (turnId: string, text: string, meta?: RuntimeProgressMeta) => Promise<void>,
38
+ ): Promise<void> {
39
+ const turn = turnStates.get(turnId);
40
+ if (!turn || !turn.onProgress) {
41
+ return;
42
+ }
43
+ for (const item of items) {
44
+ if (item?.type !== 'agentMessage' || typeof item.id !== 'string') {
45
+ continue;
46
+ }
47
+ noteAgentMessageItem(turnStates, turnId, item);
48
+ const completed = consumeCompletedAgentMessageProgress(turnStates, turnId, item.id);
49
+ if (!completed || completed.phase === 'final_answer') {
50
+ continue;
51
+ }
52
+ await safeEmitTurnProgress(turnId, completed.text, {
53
+ kind: 'agent_message',
54
+ phase: completed.phase,
55
+ turnId,
56
+ itemId: item.id,
57
+ });
58
+ }
59
+ }
60
+
61
+ export async function pollTurnFinalState(
62
+ deps: TurnFinalPollDeps,
63
+ threadId: string,
64
+ turnId: string,
65
+ options: TurnFinalPollOptions,
66
+ ): Promise<void> {
67
+ let waitingOnHumanLogged = false;
68
+ let lastWaitingStatusKey: string | null = null;
69
+ let lastSuccessfulReadAt = Date.now();
70
+ let idleSinceAt: number | null = null;
71
+ let idleAwaitingAuthoritativeResultSinceAt: number | null = null;
72
+ let idleAwaitingAuthoritativeResultLogged = false;
73
+ let lastError: Error | null = null;
74
+
75
+ while (deps.turnStates.has(turnId)) {
76
+ try {
77
+ const snapshot = await readTurnSnapshot({
78
+ request: deps.request,
79
+ cacheThreadStatus: deps.cacheThreadStatus,
80
+ }, threadId, turnId);
81
+ threadId = snapshot.threadId;
82
+ lastSuccessfulReadAt = Date.now();
83
+ lastError = null;
84
+
85
+ const items = Array.isArray(snapshot.turn?.items) ? snapshot.turn.items : [];
86
+ await observePolledTurnItems(deps.turnStates, turnId, items, deps.safeEmitTurnProgress);
87
+
88
+ if (snapshot.turn && typeof snapshot.turn.status === 'string' && snapshot.turn.status !== 'inProgress') {
89
+ const resolvedBase = {
90
+ threadId: snapshot.threadId,
91
+ turnId,
92
+ status: snapshot.turn.status,
93
+ reply: buildReplyFromThreadItems(items),
94
+ error: normalizeTurnError(snapshot.turn.error),
95
+ } satisfies RuntimeTurnResult;
96
+ const completedEventResult = deps.turnStates.get(turnId)?.completedEventResult || null;
97
+ const resolved = await enrichTerminalReply({
98
+ readPersistedFinalReply: deps.readPersistedFinalReply,
99
+ }, mergeTerminalTurnResult(completedEventResult, resolvedBase));
100
+ logInfo('runtime', 'turn_final_state_polled', {
101
+ threadId: resolved.threadId,
102
+ turnId: resolved.turnId,
103
+ status: resolved.status,
104
+ });
105
+ deps.resolveTurnResult(turnId, resolved);
106
+ return;
107
+ }
108
+
109
+ const status = snapshot.threadStatus;
110
+ if (isWaitingOnHumanStatus(status)) {
111
+ idleSinceAt = null;
112
+ idleAwaitingAuthoritativeResultSinceAt = null;
113
+ idleAwaitingAuthoritativeResultLogged = false;
114
+ const waitingStatusKey = status.activeFlags.slice().sort().join(',');
115
+ if (waitingStatusKey !== lastWaitingStatusKey) {
116
+ lastWaitingStatusKey = waitingStatusKey;
117
+ const turn = deps.turnStates.get(turnId);
118
+ await turn?.onThreadStatus?.(status, {
119
+ threadId,
120
+ turnId,
121
+ });
122
+ }
123
+ if (!waitingOnHumanLogged) {
124
+ waitingOnHumanLogged = true;
125
+ logInfo('runtime', 'turn_final_state_poll_waiting_on_human', {
126
+ threadId,
127
+ turnId,
128
+ activeFlags: status?.activeFlags || [],
129
+ });
130
+ }
131
+ await sleep(options.pollMs);
132
+ continue;
133
+ }
134
+ waitingOnHumanLogged = false;
135
+ lastWaitingStatusKey = null;
136
+
137
+ if (status?.type === 'active') {
138
+ idleSinceAt = null;
139
+ idleAwaitingAuthoritativeResultSinceAt = null;
140
+ idleAwaitingAuthoritativeResultLogged = false;
141
+ await sleep(options.pollMs);
142
+ continue;
143
+ }
144
+
145
+ if (status?.type === 'idle') {
146
+ if (idleSinceAt == null) {
147
+ idleSinceAt = Date.now();
148
+ }
149
+ if (Date.now() - idleSinceAt >= options.idleReconcileMs) {
150
+ const completedEventResult = deps.turnStates.get(turnId)?.completedEventResult || null;
151
+ if (completedEventResult) {
152
+ logInfo('runtime', 'turn_final_state_reconciled_from_completed_event', {
153
+ threadId,
154
+ turnId,
155
+ status: completedEventResult.status,
156
+ });
157
+ deps.resolveTurnResult(turnId, completedEventResult);
158
+ return;
159
+ }
160
+ if (idleAwaitingAuthoritativeResultSinceAt == null) {
161
+ idleAwaitingAuthoritativeResultSinceAt = Date.now();
162
+ }
163
+ if (!idleAwaitingAuthoritativeResultLogged) {
164
+ idleAwaitingAuthoritativeResultLogged = true;
165
+ logWarn('runtime', 'turn_reconcile_waiting_for_authoritative_result', {
166
+ threadId,
167
+ turnId,
168
+ idleReconcileMs: options.idleReconcileMs,
169
+ timeoutMs: options.idleAwaitingAuthoritativeResultTimeoutMs,
170
+ });
171
+ }
172
+ if (Date.now() - idleAwaitingAuthoritativeResultSinceAt >= options.idleAwaitingAuthoritativeResultTimeoutMs) {
173
+ const error = new Error(`runtime turn reconciliation failed: idle thread without completed turn event (${turnId})`);
174
+ logWarn('runtime', 'turn_reconcile_failed', {
175
+ threadId,
176
+ turnId,
177
+ idleDurationMs: Date.now() - idleSinceAt,
178
+ timeoutMs: options.idleAwaitingAuthoritativeResultTimeoutMs,
179
+ });
180
+ deps.rejectTurnResult(turnId, error);
181
+ return;
182
+ }
183
+ }
184
+ await sleep(options.pollMs);
185
+ continue;
186
+ }
187
+
188
+ idleSinceAt = null;
189
+ idleAwaitingAuthoritativeResultSinceAt = null;
190
+ idleAwaitingAuthoritativeResultLogged = false;
191
+ } catch (error) {
192
+ const runtimeError = normalizeRuntimeError(error);
193
+ lastError = runtimeError;
194
+ if (!deps.isRecoverableRuntimeError(runtimeError)) {
195
+ logWarn('runtime', 'turn_final_state_poll_failed', {
196
+ threadId,
197
+ turnId,
198
+ error: runtimeError.stack || runtimeError.message,
199
+ });
200
+ deps.rejectTurnResult(turnId, runtimeError);
201
+ return;
202
+ }
203
+ logWarn('runtime', 'turn_final_state_poll_failed', {
204
+ threadId,
205
+ turnId,
206
+ error: runtimeError.stack || runtimeError.message,
207
+ });
208
+ }
209
+
210
+ if (Date.now() - lastSuccessfulReadAt >= options.transportTimeoutMs) {
211
+ const error = lastError || new Error('runtime stdio is not connected');
212
+ logWarn('runtime', 'turn_final_state_poll_transport_timeout', {
213
+ threadId,
214
+ turnId,
215
+ timeoutMs: options.transportTimeoutMs,
216
+ error: error.stack || error.message,
217
+ });
218
+ deps.rejectTurnResult(turnId, error);
219
+ return;
220
+ }
221
+
222
+ await sleep(options.pollMs);
223
+ }
224
+ }