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,185 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { logInfo } from './logger.ts';
4
+ import { normalizeThreadStatus } from './runtime-client-protocol.ts';
5
+ import type { RuntimeThreadStatus, RuntimeTurnResult } from './types.ts';
6
+
7
+ interface TurnReadDeps {
8
+ request(method: string, params: unknown): Promise<any>;
9
+ cacheThreadStatus(threadId: string, status: RuntimeThreadStatus | null | undefined): RuntimeThreadStatus | null;
10
+ buildReplyFromThreadItems(items: Array<any>): string;
11
+ }
12
+
13
+ interface TerminalReplyDeps {
14
+ readPersistedFinalReply(turnId: string): string | null;
15
+ }
16
+
17
+ function collectRolloutFiles(root: string): string[] {
18
+ if (!fs.existsSync(root)) {
19
+ return [];
20
+ }
21
+ const files: string[] = [];
22
+ const stack = [root];
23
+ while (stack.length > 0) {
24
+ const current = stack.pop();
25
+ if (!current) {
26
+ continue;
27
+ }
28
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
29
+ const fullPath = path.join(current, entry.name);
30
+ if (entry.isDirectory()) {
31
+ stack.push(fullPath);
32
+ continue;
33
+ }
34
+ if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
35
+ files.push(fullPath);
36
+ }
37
+ }
38
+ }
39
+ return files.sort((left, right) => fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs);
40
+ }
41
+
42
+ export function readPersistedFinalReplyFromCodexHome(codexHome: string | null, turnId: string): string | null {
43
+ if (!codexHome) {
44
+ return null;
45
+ }
46
+ const sessionsDir = path.join(codexHome, 'sessions');
47
+ const files = collectRolloutFiles(sessionsDir);
48
+ for (const file of files) {
49
+ let content = '';
50
+ try {
51
+ content = fs.readFileSync(file, 'utf8');
52
+ } catch {
53
+ continue;
54
+ }
55
+ if (!content.includes(turnId) || !content.includes('task_complete')) {
56
+ continue;
57
+ }
58
+ const lines = content.split(/\r?\n/);
59
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
60
+ const line = lines[index];
61
+ if (!line || !line.includes(turnId) || !line.includes('task_complete')) {
62
+ continue;
63
+ }
64
+ try {
65
+ const record = JSON.parse(line);
66
+ const payload = record?.payload;
67
+ if (payload?.type !== 'task_complete' || payload?.turn_id !== turnId) {
68
+ continue;
69
+ }
70
+ const text = typeof payload.last_agent_message === 'string' ? payload.last_agent_message.trim() : '';
71
+ if (text) {
72
+ return text;
73
+ }
74
+ } catch {
75
+ continue;
76
+ }
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+
82
+ export async function enrichTerminalReply(
83
+ deps: TerminalReplyDeps,
84
+ result: RuntimeTurnResult,
85
+ ): Promise<RuntimeTurnResult> {
86
+ if (result.status !== 'completed' || String(result.reply || '').trim()) {
87
+ return result;
88
+ }
89
+ const persisted = deps.readPersistedFinalReply(result.turnId);
90
+ if (!persisted) {
91
+ return result;
92
+ }
93
+ logInfo('runtime', 'turn_reply_restored_from_task_complete', {
94
+ threadId: result.threadId,
95
+ turnId: result.turnId,
96
+ replyLength: persisted.length,
97
+ });
98
+ return {
99
+ ...result,
100
+ reply: persisted,
101
+ };
102
+ }
103
+
104
+ export function normalizeTurnError(error: unknown): string | null {
105
+ if (typeof error === 'string') {
106
+ return error;
107
+ }
108
+ if (error && typeof error === 'object' && typeof (error as { message?: unknown }).message === 'string') {
109
+ return String((error as { message?: string }).message);
110
+ }
111
+ return null;
112
+ }
113
+
114
+ export function buildTurnResultFromObject(
115
+ buildReplyFromThreadItems: (items: Array<any>) => string,
116
+ turn: any,
117
+ fallbackThreadId?: string | null,
118
+ fallbackTurnId?: string | null,
119
+ ): RuntimeTurnResult | null {
120
+ const turnId = typeof turn?.id === 'string' ? turn.id : (fallbackTurnId || null);
121
+ if (!turnId) {
122
+ return null;
123
+ }
124
+ return {
125
+ threadId: typeof turn?.threadId === 'string' ? turn.threadId : (fallbackThreadId || ''),
126
+ turnId,
127
+ status: typeof turn?.status === 'string' ? turn.status : 'completed',
128
+ reply: buildReplyFromThreadItems(Array.isArray(turn?.items) ? turn.items : []),
129
+ error: normalizeTurnError(turn?.error),
130
+ };
131
+ }
132
+
133
+ export function mergeTerminalTurnResult(
134
+ preferred: RuntimeTurnResult | null,
135
+ observed: RuntimeTurnResult,
136
+ ): RuntimeTurnResult {
137
+ if (!preferred) {
138
+ return observed;
139
+ }
140
+ return {
141
+ threadId: preferred.threadId || observed.threadId,
142
+ turnId: preferred.turnId || observed.turnId,
143
+ status: preferred.status || observed.status,
144
+ reply: preferred.reply || observed.reply,
145
+ error: preferred.error || observed.error || null,
146
+ };
147
+ }
148
+
149
+ export async function readTurnSnapshot(
150
+ deps: Pick<TurnReadDeps, 'request' | 'cacheThreadStatus'>,
151
+ threadId: string,
152
+ turnId: string,
153
+ ): Promise<{ threadId: string; threadStatus: RuntimeThreadStatus | null; turn: any | null }> {
154
+ const result = await deps.request('thread/read', { threadId, includeTurns: true });
155
+ const resolvedThreadId = result?.thread?.id || threadId;
156
+ const status = deps.cacheThreadStatus(resolvedThreadId, normalizeThreadStatus(result?.thread?.status));
157
+ if (resolvedThreadId !== threadId && status) {
158
+ deps.cacheThreadStatus(threadId, status);
159
+ }
160
+ const turns = Array.isArray(result?.thread?.turns) ? result.thread.turns : [];
161
+ const turn = turns.find((item: any) => item?.id === turnId) || null;
162
+ return {
163
+ threadId: resolvedThreadId,
164
+ threadStatus: status,
165
+ turn,
166
+ };
167
+ }
168
+
169
+ export async function readTurnResult(
170
+ deps: TurnReadDeps & TerminalReplyDeps,
171
+ threadId: string,
172
+ turnId: string,
173
+ ): Promise<RuntimeTurnResult | null> {
174
+ const snapshot = await readTurnSnapshot(deps, threadId, turnId);
175
+ if (!snapshot.turn || typeof snapshot.turn.status !== 'string') {
176
+ return null;
177
+ }
178
+ return enrichTerminalReply(deps, {
179
+ threadId: snapshot.threadId,
180
+ turnId,
181
+ status: snapshot.turn.status,
182
+ reply: deps.buildReplyFromThreadItems(Array.isArray(snapshot.turn.items) ? snapshot.turn.items : []),
183
+ error: typeof snapshot.turn.error?.message === 'string' ? snapshot.turn.error.message : null,
184
+ });
185
+ }
@@ -0,0 +1,105 @@
1
+ import type { RuntimeTurnState } from './runtime-client-agent-messages.ts';
2
+ import { logWarn } from './logger.ts';
3
+ import type { RuntimeProgressMeta, RuntimeThreadStatus, RuntimeTurnResult } from './types.ts';
4
+
5
+ function finalizeTurnState(
6
+ turnStates: Map<string, RuntimeTurnState>,
7
+ turnId: string,
8
+ ): RuntimeTurnState | null {
9
+ const turn = turnStates.get(turnId) || null;
10
+ if (!turn) {
11
+ return null;
12
+ }
13
+ turnStates.delete(turnId);
14
+ return turn;
15
+ }
16
+
17
+ export async function safeEmitRuntimeTurnProgress(
18
+ turnStates: Map<string, RuntimeTurnState>,
19
+ turnId: string,
20
+ text: string,
21
+ meta?: RuntimeProgressMeta,
22
+ ): Promise<void> {
23
+ const turn = turnStates.get(turnId);
24
+ if (!turn?.onProgress) {
25
+ return;
26
+ }
27
+ try {
28
+ await turn.onProgress(text, meta);
29
+ } catch (error) {
30
+ logWarn('runtime', 'progress_handler_failed', {
31
+ turnId,
32
+ progressKind: meta?.kind || null,
33
+ progressPhase: meta?.phase || null,
34
+ itemId: meta?.itemId || null,
35
+ error: error instanceof Error ? (error.stack || error.message) : String(error),
36
+ });
37
+ }
38
+ }
39
+
40
+ export function resolveRuntimeTurnResult(
41
+ turnStates: Map<string, RuntimeTurnState>,
42
+ cacheThreadStatus: (threadId: string, status: RuntimeThreadStatus | null | undefined) => RuntimeThreadStatus | null,
43
+ turnId: string,
44
+ result: RuntimeTurnResult,
45
+ ): boolean {
46
+ const turn = finalizeTurnState(turnStates, turnId);
47
+ if (!turn) {
48
+ return false;
49
+ }
50
+ cacheThreadStatus(result.threadId || turn.threadId, { type: 'idle', activeFlags: [] });
51
+ turn.resolve({
52
+ threadId: result.threadId || turn.threadId,
53
+ turnId: result.turnId || turnId,
54
+ status: result.status,
55
+ reply: result.reply,
56
+ error: result.error || null,
57
+ });
58
+ return true;
59
+ }
60
+
61
+ export function rejectRuntimeTurnResult(
62
+ turnStates: Map<string, RuntimeTurnState>,
63
+ turnId: string,
64
+ error: Error,
65
+ ): boolean {
66
+ const turn = finalizeTurnState(turnStates, turnId);
67
+ if (!turn) {
68
+ return false;
69
+ }
70
+ turn.reject(error);
71
+ return true;
72
+ }
73
+
74
+ export function rejectAllRuntimeTurns(
75
+ turnStates: Map<string, RuntimeTurnState>,
76
+ error: Error,
77
+ ): void {
78
+ for (const [turnId, turn] of turnStates.entries()) {
79
+ turn.reject(error);
80
+ turnStates.delete(turnId);
81
+ }
82
+ }
83
+
84
+ export function emitContextCompactionTurnProgress(
85
+ turnStates: Map<string, RuntimeTurnState>,
86
+ emitProgress: (turnId: string, text: string, meta?: RuntimeProgressMeta) => Promise<void>,
87
+ turnId: string,
88
+ itemId: string,
89
+ ): void {
90
+ const turn = turnStates.get(turnId);
91
+ if (!turn || !turn.onProgress) {
92
+ return;
93
+ }
94
+ const dedupeKey = `${turn.threadId}:${turnId}:${itemId}`;
95
+ if (turn.emittedContextCompactionKeys.has(dedupeKey)) {
96
+ return;
97
+ }
98
+ turn.emittedContextCompactionKeys.add(dedupeKey);
99
+ void emitProgress(turnId, 'Codex 正在压缩当前会话上下文。', {
100
+ kind: 'context_compaction',
101
+ phase: null,
102
+ turnId,
103
+ itemId,
104
+ });
105
+ }
@@ -0,0 +1,344 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+ import { EventEmitter } from 'node:events';
4
+ import type { RuntimeTurnState } from './runtime-client-agent-messages.ts';
5
+ import { handleRuntimeRpcMessage, type PendingRuntimeRequest } from './runtime-client-message-dispatch.ts';
6
+ import { createRunTurnEventBridge, startRuntimeTurn } from './runtime-client-run-turn.ts';
7
+ import { pollTurnFinalState } from './runtime-client-turn-poll.ts';
8
+ import { rejectAllPendingRequests, sendRuntimeRequest } from './runtime-client-request-ops.ts';
9
+ import { emitContextCompactionTurnProgress, rejectAllRuntimeTurns, rejectRuntimeTurnResult, resolveRuntimeTurnResult, safeEmitRuntimeTurnProgress } from './runtime-client-turn-state.ts';
10
+ import { RuntimeClientTransport } from './runtime-client-transport.ts';
11
+ import { cacheThreadStatusFor, cachedThreadStatusFor, interruptTurnRequest, readPersistedFinalReply, readThreadRequest, readThreadStatusRequest, recoverTurnResultRequest, resolveApprovalRequest, resolveElicitationRequest, resolveUserInputRequest, resumeThreadRequest, startThreadRequest, steerTurnRequest } from './runtime-client-thread-ops.ts';
12
+ import { logInfo } from './logger.ts';
13
+ import type {
14
+ ApprovalDecision,
15
+ ConnectionLifecycleEvent,
16
+ RuntimeProgressMeta,
17
+ RuntimeThreadStatus,
18
+ RuntimeTurnResult,
19
+ ServerRequestResolvedEvent,
20
+ } from './types.ts';
21
+ import {
22
+ normalizeRuntimeError,
23
+ runtimeErrorMessage,
24
+ type ApprovalPolicy,
25
+ type JsonRpcMessage,
26
+ type RunTurnOptions,
27
+ type ThreadOptions,
28
+ } from './runtime-client-protocol.ts';
29
+
30
+ const CONNECT_TIMEOUT_MS = Number(process.env.WORK_ALLY_CODEX_CONNECT_TIMEOUT_MS || '5000');
31
+ const DISCONNECT_TIMEOUT_MS = 1000;
32
+ const RECONNECT_BASE_DELAY_MS = Number(process.env.WORK_ALLY_RUNTIME_RECONNECT_BASE_MS || '2000');
33
+ const RECONNECT_MAX_DELAY_MS = Number(process.env.WORK_ALLY_RUNTIME_RECONNECT_MAX_MS || '30000');
34
+ const RECOVERABLE_RUNTIME_ERROR_RE = /runtime (?:stdio|transport) is not connected|runtime child process exited|runtime connect timed out|not materialized yet|includeTurns is unavailable before first user message/i;
35
+ const TURN_IDLE_RECONCILE_DELAY_MS = Number(process.env.WORK_ALLY_TURN_IDLE_RECONCILE_DELAY_MS || '500');
36
+ const TURN_FINAL_POLL_MS = Number(process.env.WORK_ALLY_TURN_FINAL_POLL_MS || '250');
37
+ const TURN_RESULT_RECOVERY_TIMEOUT_MS = Number(process.env.WORK_ALLY_TURN_RESULT_RECOVERY_TIMEOUT_MS || '60000');
38
+
39
+ interface CodexRuntimeClientOptions {
40
+ codexHome?: string | null;
41
+ codexBin?: string | null;
42
+ spawnImpl?: typeof spawn;
43
+ }
44
+
45
+ export class CodexRuntimeClient {
46
+ private readonly codexHome: string | null;
47
+ private readonly codexBin: string;
48
+ private readonly spawnImpl: typeof spawn;
49
+ private readonly transport: RuntimeClientTransport;
50
+ private nextId = 1;
51
+ private readonly pending = new Map<string | number, PendingRuntimeRequest>();
52
+ private readonly turnStates = new Map<string, RuntimeTurnState>();
53
+ private readonly emitter = new EventEmitter();
54
+ private readonly threadStatusCache = new Map<string, RuntimeThreadStatus>();
55
+
56
+ constructor(options: CodexRuntimeClientOptions = {}) {
57
+ this.codexHome = options.codexHome ? path.resolve(options.codexHome) : null;
58
+ this.codexBin = (options.codexBin || process.env.WORK_ALLY_CODEX_BIN || 'codex').trim() || 'codex';
59
+ this.spawnImpl = options.spawnImpl || spawn;
60
+ this.transport = new RuntimeClientTransport({
61
+ codexHome: this.codexHome,
62
+ codexBin: this.codexBin,
63
+ spawnImpl: this.spawnImpl,
64
+ connectTimeoutMs: CONNECT_TIMEOUT_MS,
65
+ disconnectTimeoutMs: DISCONNECT_TIMEOUT_MS,
66
+ reconnectBaseDelayMs: RECONNECT_BASE_DELAY_MS,
67
+ reconnectMaxDelayMs: RECONNECT_MAX_DELAY_MS,
68
+ initialize: () => this.initialize(),
69
+ onMessage: (line) => this.onMessage(line),
70
+ rejectAllPending: (error) => this.rejectAllPending(error),
71
+ rejectAllTurns: (error) => this.rejectAllTurns(error),
72
+ activeTurnCount: () => this.turnStates.size,
73
+ emitConnectionEvent: (event) => this.emitConnectionEvent(event),
74
+ });
75
+ }
76
+
77
+ private emitConnectionEvent(event: ConnectionLifecycleEvent): void {
78
+ this.emitter.emit('connectionLifecycle', event);
79
+ }
80
+
81
+ onConnectionEvent(listener: (event: ConnectionLifecycleEvent) => void): void {
82
+ this.emitter.on('connectionLifecycle', listener);
83
+ }
84
+
85
+ offConnectionEvent(listener: (event: ConnectionLifecycleEvent) => void): void {
86
+ this.emitter.off('connectionLifecycle', listener);
87
+ }
88
+
89
+ onServerRequestResolved(listener: (event: ServerRequestResolvedEvent) => void): void {
90
+ this.emitter.on('serverRequestResolved', listener);
91
+ }
92
+
93
+ offServerRequestResolved(listener: (event: ServerRequestResolvedEvent) => void): void {
94
+ this.emitter.off('serverRequestResolved', listener);
95
+ }
96
+
97
+ private scheduleReconnect(reason: string): void {
98
+ this.transport.scheduleReconnect(reason, () => this.connect());
99
+ }
100
+
101
+
102
+ private handleTransportClosed(reason: string): void {
103
+ this.transport.handleTransportClosed(reason, (message) => this.scheduleReconnect(message));
104
+ }
105
+
106
+ private isRecoverableRuntimeError(error: unknown): boolean {
107
+ return RECOVERABLE_RUNTIME_ERROR_RE.test(runtimeErrorMessage(error));
108
+ }
109
+
110
+ private sendRaw(payload: unknown): void {
111
+ this.transport.sendRaw(payload);
112
+ }
113
+
114
+ private rejectAllPending(error: Error): void {
115
+ rejectAllPendingRequests(this.pending, error);
116
+ }
117
+
118
+ private resolveTurnResult(turnId: string, result: RuntimeTurnResult): boolean {
119
+ return resolveRuntimeTurnResult(this.turnStates, (threadId, status) => this.cacheThreadStatus(threadId, status), turnId, result);
120
+ }
121
+
122
+ private rejectTurnResult(turnId: string, error: Error): boolean {
123
+ return rejectRuntimeTurnResult(this.turnStates, turnId, error);
124
+ }
125
+
126
+ private startTurnFinalPoll(threadId: string, turnId: string): void {
127
+ const pollMs = Math.max(50, TURN_FINAL_POLL_MS);
128
+ void pollTurnFinalState({
129
+ turnStates: this.turnStates,
130
+ request: (method, params) => this.request(method, params),
131
+ cacheThreadStatus: (currentThreadId, status) => this.cacheThreadStatus(currentThreadId, status),
132
+ readPersistedFinalReply: (currentTurnId) => this.readPersistedFinalReply(currentTurnId),
133
+ resolveTurnResult: (currentTurnId, result) => this.resolveTurnResult(currentTurnId, result),
134
+ rejectTurnResult: (currentTurnId, error) => this.rejectTurnResult(currentTurnId, error),
135
+ isRecoverableRuntimeError: (error) => this.isRecoverableRuntimeError(error),
136
+ safeEmitTurnProgress: (currentTurnId, text, meta) => this.safeEmitTurnProgress(currentTurnId, text, meta),
137
+ }, threadId, turnId, {
138
+ pollMs,
139
+ transportTimeoutMs: Math.max(pollMs, TURN_RESULT_RECOVERY_TIMEOUT_MS),
140
+ idleReconcileMs: Math.max(pollMs * 2, TURN_IDLE_RECONCILE_DELAY_MS),
141
+ idleAwaitingAuthoritativeResultTimeoutMs: Math.max(Math.max(pollMs * 2, TURN_IDLE_RECONCILE_DELAY_MS), TURN_RESULT_RECOVERY_TIMEOUT_MS),
142
+ });
143
+ }
144
+
145
+ private async safeEmitTurnProgress(turnId: string, text: string, meta?: RuntimeProgressMeta): Promise<void> {
146
+ return safeEmitRuntimeTurnProgress(this.turnStates, turnId, text, meta);
147
+ }
148
+
149
+ private rejectAllTurns(error: Error): void {
150
+ rejectAllRuntimeTurns(this.turnStates, error);
151
+ }
152
+
153
+ private async request(method: string, params: unknown): Promise<any> {
154
+ return sendRuntimeRequest({
155
+ connect: () => this.connect(),
156
+ createId: () => `req_${this.nextId++}`,
157
+ pending: this.pending,
158
+ sendRaw: (payload) => this.sendRaw(payload),
159
+ }, method, params);
160
+ }
161
+
162
+ private async initialize(): Promise<void> {
163
+ await this.request('initialize', {
164
+ clientInfo: { name: 'work-ally-bridge', version: '0.1.0' },
165
+ capabilities: { experimentalApi: true },
166
+ });
167
+ }
168
+
169
+
170
+ private cacheThreadStatus(threadId: string, status: RuntimeThreadStatus | null | undefined): RuntimeThreadStatus | null {
171
+ return cacheThreadStatusFor(this.threadStatusCache, threadId, status);
172
+ }
173
+
174
+ private cachedThreadStatus(threadId: string): RuntimeThreadStatus | null {
175
+ return cachedThreadStatusFor(this.threadStatusCache, threadId);
176
+ }
177
+
178
+ private emitContextCompactionProgress(turnId: string, itemId: string): void {
179
+ emitContextCompactionTurnProgress(
180
+ this.turnStates,
181
+ (currentTurnId, text, meta) => this.safeEmitTurnProgress(currentTurnId, text, meta),
182
+ turnId,
183
+ itemId,
184
+ );
185
+ }
186
+
187
+ readPersistedFinalReply(turnId: string): string | null {
188
+ return readPersistedFinalReply(this.codexHome, turnId);
189
+ }
190
+
191
+ async connect(): Promise<void> {
192
+ return this.transport.connect();
193
+ }
194
+
195
+ async disconnect(): Promise<void> {
196
+ return this.transport.disconnect();
197
+ }
198
+
199
+ private onMessage(data: unknown): void {
200
+ const text = typeof data === 'string' ? data : Buffer.from(data as ArrayBuffer).toString('utf8');
201
+ const msg = JSON.parse(text) as JsonRpcMessage;
202
+ handleRuntimeRpcMessage(msg, {
203
+ pending: this.pending,
204
+ turnStates: this.turnStates,
205
+ cacheThreadStatus: (threadId, status) => this.cacheThreadStatus(threadId, status),
206
+ emitServerRequestResolved: (event) => this.emitter.emit('serverRequestResolved', event),
207
+ emitServerRequest: (message) => this.emitter.emit('serverRequest', message),
208
+ emitContextCompactionProgress: (turnId, itemId) => this.emitContextCompactionProgress(turnId, itemId),
209
+ resolveTurnResult: (turnId, result) => this.resolveTurnResult(turnId, result),
210
+ });
211
+ }
212
+
213
+ async healthcheck(): Promise<boolean> {
214
+ try {
215
+ await this.connect();
216
+ return true;
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+
222
+ async startThread(cwd: string, options: ThreadOptions = {}): Promise<{ id: string }> {
223
+ return startThreadRequest({
224
+ request: (method, params) => this.request(method, params),
225
+ cacheThreadStatus: (threadId, status) => this.cacheThreadStatus(threadId, status),
226
+ }, cwd, options);
227
+ }
228
+
229
+ async resumeThread(threadId: string, cwd: string, options: ThreadOptions = {}): Promise<{ id: string }> {
230
+ return resumeThreadRequest({
231
+ request: (method, params) => this.request(method, params),
232
+ cacheThreadStatus: (resolvedThreadId, status) => this.cacheThreadStatus(resolvedThreadId, status),
233
+ startThread: (nextCwd, nextOptions = {}) => this.startThread(nextCwd, nextOptions),
234
+ }, threadId, cwd, options);
235
+ }
236
+
237
+ async readThread(threadId: string, includeTurns = false): Promise<{ id: string; status: RuntimeThreadStatus | null }> {
238
+ const result = await readThreadRequest({
239
+ request: (method, params) => this.request(method, params),
240
+ cacheThreadStatus: (resolvedThreadId, status) => this.cacheThreadStatus(resolvedThreadId, status),
241
+ }, threadId, includeTurns);
242
+ return { id: result.id, status: result.status };
243
+ }
244
+
245
+ async readThreadWithTurns(threadId: string): Promise<{ id: string; status: RuntimeThreadStatus | null; turns: any[] }> {
246
+ const result = await readThreadRequest({
247
+ request: (method, params) => this.request(method, params),
248
+ cacheThreadStatus: (resolvedThreadId, status) => this.cacheThreadStatus(resolvedThreadId, status),
249
+ }, threadId, true);
250
+ return {
251
+ id: result.id,
252
+ status: result.status,
253
+ turns: Array.isArray(result.turns) ? result.turns : [],
254
+ };
255
+ }
256
+
257
+ async readThreadStatus(threadId: string): Promise<RuntimeThreadStatus | null> {
258
+ return readThreadStatusRequest({
259
+ request: (method, params) => this.request(method, params),
260
+ cacheThreadStatus: (resolvedThreadId, status) => this.cacheThreadStatus(resolvedThreadId, status),
261
+ cachedThreadStatus: (currentThreadId) => this.cachedThreadStatus(currentThreadId),
262
+ }, threadId);
263
+ }
264
+
265
+ async interruptTurn(threadId: string, turnId: string): Promise<void> {
266
+ await interruptTurnRequest((method, params) => this.request(method, params), threadId, turnId);
267
+ }
268
+
269
+ async steerTurn(threadId: string, turnId: string, prompt: string): Promise<void> {
270
+ await steerTurnRequest({
271
+ connect: () => this.connect(),
272
+ request: (method, params) => this.request(method, params),
273
+ isRecoverableRuntimeError: (error) => this.isRecoverableRuntimeError(error),
274
+ scheduleReconnect: (reason) => this.scheduleReconnect(reason),
275
+ }, threadId, turnId, prompt);
276
+ }
277
+
278
+ async recoverTurnResult(
279
+ threadId: string,
280
+ turnId: string,
281
+ options: { timeoutMs?: number; pollMs?: number } = {},
282
+ ): Promise<RuntimeTurnResult | null> {
283
+ return recoverTurnResultRequest({
284
+ request: (method, params) => this.request(method, params),
285
+ cacheThreadStatus: (resolvedThreadId, status) => this.cacheThreadStatus(resolvedThreadId, status),
286
+ readPersistedFinalReply: (currentTurnId) => this.readPersistedFinalReply(currentTurnId),
287
+ }, threadId, turnId, options);
288
+ }
289
+
290
+ async resolveApproval(requestId: string | number, decision: ApprovalDecision): Promise<void> {
291
+ resolveApprovalRequest((payload) => this.sendRaw(payload), requestId, decision);
292
+ }
293
+
294
+ async resolveUserInput(
295
+ requestId: string | number,
296
+ response: { answers: Record<string, { answers: string[] }> },
297
+ ): Promise<void> {
298
+ resolveUserInputRequest((payload) => this.sendRaw(payload), requestId, response);
299
+ }
300
+
301
+ async resolveElicitation(
302
+ requestId: string | number,
303
+ response: { action: 'accept' | 'decline' | 'cancel'; content: unknown | null },
304
+ ): Promise<void> {
305
+ resolveElicitationRequest((payload) => this.sendRaw(payload), requestId, response);
306
+ }
307
+
308
+ async runTurn(options: RunTurnOptions): Promise<RuntimeTurnResult> {
309
+ await this.connect();
310
+
311
+ let activeTurnId: string | null = null;
312
+
313
+ const { progressHandler, serverRequestHandler } = createRunTurnEventBridge({
314
+ safeEmitTurnProgress: (turnId, progressText, meta) => this.safeEmitTurnProgress(turnId, progressText, meta),
315
+ resolveApproval: (requestId, decision) => this.resolveApproval(requestId, decision),
316
+ resolveUserInput: (requestId, response) => this.resolveUserInput(requestId, response),
317
+ resolveElicitation: (requestId, response) => this.resolveElicitation(requestId, response),
318
+ }, options, {
319
+ getActiveTurnId: () => activeTurnId,
320
+ });
321
+ this.emitter.on('progress', progressHandler);
322
+ this.emitter.on('serverRequest', serverRequestHandler);
323
+ try {
324
+ const { turnId, turnPromise } = await startRuntimeTurn({
325
+ request: (method, params) => this.request(method, params),
326
+ cacheThreadStatus: (threadId, status) => this.cacheThreadStatus(threadId, status),
327
+ turnStates: this.turnStates,
328
+ startTurnFinalPoll: (threadId, startedTurnId) => this.startTurnFinalPoll(threadId, startedTurnId),
329
+ rejectTurnResult: (turnId, error) => this.rejectTurnResult(turnId, error),
330
+ }, options);
331
+ activeTurnId = turnId;
332
+ return await turnPromise;
333
+ } catch (error) {
334
+ if (this.isRecoverableRuntimeError(error)) {
335
+ const message = error instanceof Error ? error.message : String(error);
336
+ this.scheduleReconnect(message);
337
+ }
338
+ throw error;
339
+ } finally {
340
+ this.emitter.off('progress', progressHandler);
341
+ this.emitter.off('serverRequest', serverRequestHandler);
342
+ }
343
+ }
344
+ }