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,329 @@
1
+ import path from 'node:path';
2
+ import { formatCommandApprovalRulesStatusLines, loadCommandApprovalRules, type CommandApprovalRuleset } from './approval-rules.ts';
3
+ import type { ChannelAdapter, OutboundEnvelope } from './channel-types.ts';
4
+ import { errorDetailsFromUnknown } from './receiver-helpers.ts';
5
+ import { replayHiddenApprovalNotice, replayHiddenPendingUserInput, runtimeWaitingStateFor, waitingStateFor } from './receiver-human-gate.ts';
6
+ import { isActiveTurnStale, runtimeRecoveryKindFor } from './receiver-recovery.ts';
7
+ import { handleControlCommand } from './receiver-control.ts';
8
+ import { handleInboundPreflight } from './receiver-inbound-preflight.ts';
9
+ import { prepareInboundThreadStart } from './receiver-thread-start.ts';
10
+ import type { WorkAllyConfig } from './config.ts';
11
+ import { parseApprovalReply, parseControlCommand } from './router.ts';
12
+ import { logInfo, logWarn } from './logger.ts';
13
+ import { clearBridgeDeliveryIssue, describeChannelDeliveryIssue, isNonFatalChannelDeliveryError, recordBridgeDeliveryIssue } from './channel-delivery.ts';
14
+ import type { SessionStore } from './session-store.ts';
15
+ import { executeInboundTurn } from './receiver-turn-execution.ts';
16
+ import { createRuntimeRecoveryRegistry, readCurrentRuntimeThreadStatus } from './receiver-runtime-state.ts';
17
+ import { steerActiveTurn } from './receiver-turn-steer.ts';
18
+ import { createReceiverSupportActions } from './receiver-support-actions.ts';
19
+ import { HandoffService } from './handoff-service.ts';
20
+ import { ensureLinkedWorkSessionForThread, syncLinkedWorkSessionThread } from './receiver-work-session.ts';
21
+ import { syncThreadConversationById } from './thread-sync.ts';
22
+ import { WorkSessionStore } from './work-session-store.ts';
23
+ import type {
24
+ ConnectionLifecycleEvent,
25
+ ConversationRef,
26
+ InboundMessage,
27
+ RuntimeThreadStatus,
28
+ SessionMeta,
29
+ ServerRequestResolvedEvent,
30
+ RuntimeProgressMeta,
31
+ RuntimeTurnResult,
32
+ UserInputRequest,
33
+ } from './types.ts';
34
+
35
+ interface RuntimeLike {
36
+ startThread(cwd: string): Promise<{ id: string }>;
37
+ resumeThread(threadId: string, cwd: string): Promise<{ id: string }>;
38
+ runTurn(options: {
39
+ threadId: string;
40
+ prompt: string;
41
+ messageId: string;
42
+ cwd: string;
43
+ onTurnStarted?: (turnId: string) => Promise<void> | void;
44
+ onProgress?: (text: string, meta?: RuntimeProgressMeta) => Promise<void> | void;
45
+ onThreadStatus?: (status: RuntimeThreadStatus, meta?: { threadId?: string | null; turnId?: string | null }) => Promise<void> | void;
46
+ onApproval?: (approval: any) => Promise<void> | void;
47
+ onUserInput?: (request: UserInputRequest) => Promise<void> | void;
48
+ approvalPolicy?: 'untrusted' | 'on-failure' | 'on-request' | 'never';
49
+ sandboxPolicy?: unknown;
50
+ }): Promise<RuntimeTurnResult>;
51
+ resolveApproval(requestId: string | number, decision: 'accept' | 'acceptForSession' | 'decline' | 'cancel'): Promise<void>;
52
+ resolveUserInput(requestId: string | number, response: { answers: Record<string, { answers: string[] }> }): Promise<void>;
53
+ resolveElicitation(
54
+ requestId: string | number,
55
+ response: { action: 'accept' | 'decline' | 'cancel'; content: unknown | null },
56
+ ): Promise<void>;
57
+ steerTurn?(threadId: string, turnId: string, prompt: string): Promise<void>;
58
+ interruptTurn(threadId: string, turnId: string): Promise<void>;
59
+ readThreadStatus(threadId: string): Promise<RuntimeThreadStatus | null>;
60
+ readThreadWithTurns?(threadId: string): Promise<{ id: string; status: RuntimeThreadStatus | null; turns: any[] }>;
61
+ recoverTurnResult?(threadId: string, turnId: string, options?: { timeoutMs?: number; pollMs?: number }): Promise<RuntimeTurnResult | null>;
62
+ onConnectionEvent?(listener: (event: ConnectionLifecycleEvent) => void): void;
63
+ offConnectionEvent?(listener: (event: ConnectionLifecycleEvent) => void): void;
64
+ onServerRequestResolved?(listener: (event: ServerRequestResolvedEvent) => void): void;
65
+ offServerRequestResolved?(listener: (event: ServerRequestResolvedEvent) => void): void;
66
+ }
67
+
68
+ export class Receiver {
69
+ private readonly config: WorkAllyConfig;
70
+ private readonly channel: ChannelAdapter;
71
+ private readonly sessionStore: SessionStore;
72
+ private readonly runtime: RuntimeLike;
73
+ private readonly workSessionStore: WorkSessionStore;
74
+ private readonly handoffService: HandoffService;
75
+ private readonly connectionLifecycleListener: (event: ConnectionLifecycleEvent) => void;
76
+ private readonly serverRequestResolvedListener: (event: ServerRequestResolvedEvent) => void;
77
+ private readonly suppressedInterruptedTurnIds = new Set<string>();
78
+ private connectionLifecycleQueue: Promise<void> = Promise.resolve();
79
+ private readonly runtimeRecovery: ReturnType<typeof createRuntimeRecoveryRegistry>;
80
+ private readonly userRequestedStopTurnIds = new Set<string>();
81
+ private readonly approvalNoticeTimers = new Map<string, NodeJS.Timeout>();
82
+ private readonly commandApprovalRules: CommandApprovalRuleset;
83
+ private readonly support: ReturnType<typeof createReceiverSupportActions>;
84
+
85
+ constructor(
86
+ config: WorkAllyConfig,
87
+ channel: ChannelAdapter,
88
+ sessionStore: SessionStore,
89
+ runtimeOrLegacyArchive: RuntimeLike,
90
+ runtimeOrWorkSessionStore?: RuntimeLike | WorkSessionStore,
91
+ workSessionStoreMaybe?: WorkSessionStore,
92
+ ) {
93
+ const runtime = runtimeOrWorkSessionStore && typeof (runtimeOrWorkSessionStore as RuntimeLike).startThread === 'function'
94
+ ? runtimeOrWorkSessionStore as RuntimeLike
95
+ : runtimeOrLegacyArchive;
96
+ const workSessionStore = runtimeOrWorkSessionStore && typeof (runtimeOrWorkSessionStore as RuntimeLike).startThread === 'function'
97
+ ? workSessionStoreMaybe
98
+ : runtimeOrWorkSessionStore as WorkSessionStore | undefined;
99
+
100
+ this.config = config;
101
+ this.channel = channel;
102
+ this.sessionStore = sessionStore;
103
+ this.runtime = runtime;
104
+ this.workSessionStore = workSessionStore || new WorkSessionStore(this.config.paths.workSessionsDir || path.join(this.config.paths.runtimeDir, 'work-sessions'));
105
+ this.handoffService = new HandoffService(this.config, this.workSessionStore);
106
+ this.runtimeRecovery = createRuntimeRecoveryRegistry(this.sessionStore);
107
+ this.commandApprovalRules = loadCommandApprovalRules({
108
+ assistantCodexHome: this.assistantCodexHome(),
109
+ });
110
+ this.support = createReceiverSupportActions({
111
+ config: this.config,
112
+ sessionStore: this.sessionStore,
113
+ runtime: this.runtime,
114
+ runtimeRecovery: this.runtimeRecovery,
115
+ approvalNoticeTimers: this.approvalNoticeTimers,
116
+ commandApprovalRules: this.commandApprovalRules,
117
+ send: this.send.bind(this),
118
+ });
119
+
120
+ this.connectionLifecycleListener = (event) => {
121
+ this.connectionLifecycleQueue = this.connectionLifecycleQueue
122
+ .catch(() => undefined)
123
+ .then(() => this.support.handleConnectionLifecycle(event))
124
+ .catch((error) => {
125
+ logWarn('receiver', 'connection_lifecycle_notice_failed', {
126
+ source: event.source,
127
+ state: event.state,
128
+ error: errorDetailsFromUnknown(error),
129
+ });
130
+ });
131
+ };
132
+ this.serverRequestResolvedListener = (event) => {
133
+ void this.support.handleServerRequestResolved(event);
134
+ };
135
+ this.runtime.onConnectionEvent?.(this.connectionLifecycleListener);
136
+ this.runtime.onServerRequestResolved?.(this.serverRequestResolvedListener);
137
+ this.channel.onConnectionEvent?.(this.connectionLifecycleListener);
138
+ }
139
+
140
+ private assistantName(): string {
141
+ return String(this.config.assistant?.name || 'native').trim() || 'native';
142
+ }
143
+
144
+ private assistantCodexHome(): string | null {
145
+ const codexHome = String(this.config.assistant?.codexHome || '').trim();
146
+ return codexHome || null;
147
+ }
148
+
149
+ private async send(
150
+ outbound: OutboundEnvelope,
151
+ extra: Record<string, unknown> = {},
152
+ delivery?: { trackTurnId?: string | null },
153
+ ): Promise<boolean> {
154
+ try {
155
+ await this.channel.send(outbound);
156
+ clearBridgeDeliveryIssue(this.config.paths.runtimeDir);
157
+ if (delivery?.trackTurnId) {
158
+ this.sessionStore.markTurnDelivery(outbound.conversationRef, delivery.trackTurnId, 'delivered');
159
+ }
160
+ if (outbound.type === 'final_reply') {
161
+ const session = this.sessionStore.getSession(outbound.conversationRef);
162
+ if (session?.threadId && typeof this.runtime.readThreadWithTurns === 'function') {
163
+ syncThreadConversationById(this.config, this.runtime, session.threadId).catch((error) => {
164
+ logWarn('receiver', 'thread_sync_after_reply_failed', {
165
+ threadId: session.threadId,
166
+ conversationId: outbound.conversationRef.conversationId,
167
+ error: errorDetailsFromUnknown(error),
168
+ });
169
+ });
170
+ }
171
+ }
172
+ return true;
173
+ } catch (error) {
174
+ if (isNonFatalChannelDeliveryError(error)) {
175
+ recordBridgeDeliveryIssue(this.config.paths.runtimeDir, error);
176
+ logWarn('receiver', 'channel_delivery_unavailable', {
177
+ conversationId: outbound.conversationRef.conversationId,
178
+ envelopeType: outbound.type,
179
+ ...describeChannelDeliveryIssue(error),
180
+ });
181
+ if (delivery?.trackTurnId) {
182
+ this.sessionStore.markTurnDelivery(outbound.conversationRef, delivery.trackTurnId, 'delivery_unavailable');
183
+ }
184
+ return false;
185
+ }
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ private async handleControl(message: InboundMessage): Promise<boolean> {
191
+ const command = parseControlCommand(message.text) || parseApprovalReply(message.text, message.replyToText);
192
+ if (!command) {
193
+ return false;
194
+ }
195
+
196
+ return handleControlCommand({
197
+ config: this.config,
198
+ runtime: this.runtime,
199
+ sessionStore: this.sessionStore,
200
+ workSessionStore: this.workSessionStore,
201
+ handoffService: this.handoffService,
202
+ send: this.send.bind(this),
203
+ assistantName: this.assistantName(),
204
+ assistantCodexHome: this.assistantCodexHome(),
205
+ commandApprovalRulesStatusLines: formatCommandApprovalRulesStatusLines(this.commandApprovalRules),
206
+ noteUserRequestedStop: (turnId) => this.userRequestedStopTurnIds.add(turnId),
207
+ }, {
208
+ message,
209
+ command,
210
+ session: this.sessionStore.ensureSession(message.conversationRef),
211
+ });
212
+ }
213
+
214
+ async redeliverPendingFinalReplies(conversationRef: InboundMessage['conversationRef']): Promise<number> {
215
+ return this.support.redeliverPendingFinalReplies(conversationRef);
216
+ }
217
+
218
+ async handleInbound(message: InboundMessage): Promise<void> {
219
+ logInfo('receiver', 'inbound_received', {
220
+ messageId: message.messageId,
221
+ conversationId: message.conversationRef.conversationId,
222
+ userId: message.conversationRef.userId,
223
+ textPreview: message.text.slice(0, 120),
224
+ isControl: Boolean(parseControlCommand(message.text)),
225
+ });
226
+ this.sessionStore.noteInboundObserved(message.conversationRef, message.messageId);
227
+
228
+ const receipt = this.sessionStore.noteInboundReceipt(message.conversationRef, message.messageId);
229
+ if (receipt.status === 'duplicate_completed') {
230
+ logWarn('receiver', 'inbound_duplicate_completed', {
231
+ messageId: message.messageId,
232
+ conversationId: message.conversationRef.conversationId,
233
+ });
234
+ return;
235
+ }
236
+ if (receipt.status === 'duplicate_superseded') {
237
+ logWarn('receiver', 'inbound_duplicate_superseded', {
238
+ messageId: message.messageId,
239
+ conversationId: message.conversationRef.conversationId,
240
+ });
241
+ return;
242
+ }
243
+ if (receipt.status === 'duplicate_processing') {
244
+ logWarn('receiver', 'inbound_duplicate_processing', {
245
+ messageId: message.messageId,
246
+ conversationId: message.conversationRef.conversationId,
247
+ });
248
+ return;
249
+ }
250
+
251
+ let shouldCompleteReceipt = false;
252
+ try {
253
+ if (await this.handleControl(message)) {
254
+ shouldCompleteReceipt = true;
255
+ return;
256
+ }
257
+
258
+ const preflight = await handleInboundPreflight({
259
+ sessionStore: this.sessionStore,
260
+ send: this.send.bind(this),
261
+ maybeResumePendingUserInput: (inbound) => this.support.maybeResumePendingUserInput(inbound),
262
+ replayHiddenPendingApproval: (ref, threadId, turnId) => replayHiddenApprovalNotice({
263
+ sessionStore: this.sessionStore,
264
+ send: this.send.bind(this),
265
+ approvalBoundaryReason: (approval) => this.support.approvalBoundaryReason(approval),
266
+ }, ref, threadId, turnId),
267
+ replayHiddenPendingUserInput: (ref, threadId, turnId) => replayHiddenPendingUserInput({
268
+ sessionStore: this.sessionStore,
269
+ send: this.send.bind(this),
270
+ }, ref, threadId, turnId),
271
+ redeliverPendingTurnResult: (ref, turnId) => this.support.redeliverPendingTurnResult(ref, turnId),
272
+ currentRuntimeThreadStatus: (session) => readCurrentRuntimeThreadStatus({ runtime: this.runtime }, session),
273
+ waitingStateFor: (session) => waitingStateFor(session),
274
+ runtimeWaitingStateFor: (runtimeStatus) => runtimeWaitingStateFor(runtimeStatus),
275
+ runtimeRecoveryKindFor: (text) => runtimeRecoveryKindFor(text),
276
+ maybeHandleWaitingApprovalShortcut: (inbound, session) => this.support.maybeHandleWaitingApprovalShortcut(inbound, session),
277
+ isActiveTurnStale: (runtimeStatus) => isActiveTurnStale(runtimeStatus),
278
+ clearRuntimeRecovery: (ref) => this.runtimeRecovery.clear(ref),
279
+ steerActiveTurn: (inbound, session, runtimeWaitingState) => steerActiveTurn({
280
+ sessionStore: this.sessionStore,
281
+ runtime: this.runtime,
282
+ send: this.send.bind(this),
283
+ reactEmoji: this.config.feishu.reactEmoji,
284
+ }, inbound, session, runtimeWaitingState),
285
+ }, message);
286
+ if (preflight.kind === 'handled') {
287
+ shouldCompleteReceipt = true;
288
+ return;
289
+ }
290
+
291
+ const session = preflight.session;
292
+
293
+ const thread = await prepareInboundThreadStart({
294
+ config: this.config,
295
+ runtime: this.runtime,
296
+ sessionStore: this.sessionStore,
297
+ send: this.send.bind(this),
298
+ clearRuntimeRecovery: (ref) => this.runtimeRecovery.clear(ref),
299
+ }, message);
300
+ ensureLinkedWorkSessionForThread({
301
+ assistantName: this.assistantName(),
302
+ assistantCodexHome: this.assistantCodexHome(),
303
+ workspaceRoot: this.config.workspaceRoot,
304
+ workSessionStore: this.workSessionStore,
305
+ }, message.conversationRef, thread.id);
306
+
307
+ shouldCompleteReceipt = await executeInboundTurn({
308
+ config: this.config,
309
+ sessionStore: this.sessionStore,
310
+ runtime: this.runtime,
311
+ runtimeRecovery: this.runtimeRecovery,
312
+ support: this.support,
313
+ send: this.send.bind(this),
314
+ userRequestedStopTurnIds: this.userRequestedStopTurnIds,
315
+ suppressedInterruptedTurnIds: this.suppressedInterruptedTurnIds,
316
+ }, {
317
+ message,
318
+ fallbackSession: session,
319
+ threadId: thread.id,
320
+ });
321
+ } finally {
322
+ if (shouldCompleteReceipt) {
323
+ this.sessionStore.completeInboundReceipt(message.conversationRef, message.messageId);
324
+ } else {
325
+ this.sessionStore.clearInboundReceipt(message.conversationRef, message.messageId);
326
+ }
327
+ }
328
+ }
329
+ }
@@ -0,0 +1,62 @@
1
+ export type ControlCommand =
2
+ | { type: 'new' }
3
+ | { type: 'takeover'; threadHandle: string | null }
4
+ | { type: 'threads' }
5
+ | { type: 'codex' }
6
+ | { type: 'status' }
7
+ | { type: 'codex_configure' }
8
+ | { type: 'stop' }
9
+ | { type: 'approve'; approvalId: string }
10
+ | { type: 'deny'; approvalId: string }
11
+ | null;
12
+
13
+ export function parseControlCommand(input: string): ControlCommand {
14
+ const trimmed = input.trim();
15
+ if (trimmed === '/new') return { type: 'new' };
16
+ const takeover = trimmed.match(/^\/takeover(?:\s+([a-z0-9][a-z0-9-]{2,31}))?$/i);
17
+ if (takeover) return { type: 'takeover', threadHandle: takeover[1] ? takeover[1].toLowerCase() : null };
18
+ if (trimmed === '/threads') return { type: 'threads' };
19
+ if (trimmed === '/codex') return { type: 'codex' };
20
+ if (trimmed === '/status') return { type: 'status' };
21
+ if (trimmed === '/codex-configure') return { type: 'codex_configure' };
22
+ if (trimmed === '/stop') return { type: 'stop' };
23
+ const approve = trimmed.match(/^\/approve\s+([\w.-]+)$/);
24
+ if (approve) return { type: 'approve', approvalId: approve[1] };
25
+ const deny = trimmed.match(/^\/deny\s+([\w.-]+)$/);
26
+ if (deny) return { type: 'deny', approvalId: deny[1] };
27
+ return null;
28
+ }
29
+
30
+ export function parseApprovalShortcut(input: string): 'approve' | 'deny' | null {
31
+ const trimmed = input.trim();
32
+ if (trimmed === 'OK' || trimmed === '同意') return 'approve';
33
+ if (trimmed === 'NO' || trimmed === '拒绝') return 'deny';
34
+ return null;
35
+ }
36
+
37
+ const APPROVAL_ID_RE = /审批编号:([\w.-]+)|approval_id:\s*([\w.-]+)/i;
38
+ const APPROVE_WORDS = new Set(['同意', '批准', '允许', 'approve', 'approved', 'yes', 'y', 'ok', '好的', '可以', '✅', '👍', '👌']);
39
+ const DENY_WORDS = new Set(['拒绝', '不同意', '驳回', 'deny', 'decline', 'declined', 'reject', 'rejected', 'no', 'n', '❌', '👎']);
40
+
41
+ function normalizeDecision(input: string): string {
42
+ return input.trim().replace(/[。..!!??,,、\s]+$/g, '').toLowerCase();
43
+ }
44
+
45
+ export function parseApprovalReply(input: string, replyToText?: string | null): { type: 'approve' | 'deny'; approvalId: string } | null {
46
+ const match = String(replyToText || '').match(APPROVAL_ID_RE);
47
+ const approvalId = match?.[1] || match?.[2] || '';
48
+ if (!approvalId) {
49
+ return null;
50
+ }
51
+ const normalized = normalizeDecision(input);
52
+ if (!normalized) {
53
+ return null;
54
+ }
55
+ if (APPROVE_WORDS.has(normalized)) {
56
+ return { type: 'approve', approvalId };
57
+ }
58
+ if (DENY_WORDS.has(normalized)) {
59
+ return { type: 'deny', approvalId };
60
+ }
61
+ return null;
62
+ }
@@ -0,0 +1,150 @@
1
+ import { logInfo } from './logger.ts';
2
+ import { isContextCompactionType, normalizeAgentMessagePhase, type AgentMessagePhase, type JsonRpcMessage } from './runtime-client-protocol.ts';
3
+ import type { RuntimeProgressMeta, RuntimeThreadStatus, RuntimeTurnResult } from './types.ts';
4
+
5
+ export interface AgentMessageState {
6
+ order: number;
7
+ phase: AgentMessagePhase;
8
+ buffer: string;
9
+ completedText: string | null;
10
+ progressSentText: string | null;
11
+ }
12
+
13
+ export interface RuntimeTurnState {
14
+ threadId: string;
15
+ agentMessages: Map<string, AgentMessageState>;
16
+ nextAgentMessageOrder: number;
17
+ onProgress?: (text: string, meta?: RuntimeProgressMeta) => Promise<void> | void;
18
+ onThreadStatus?: (status: RuntimeThreadStatus, meta?: { threadId?: string | null; turnId?: string | null }) => Promise<void> | void;
19
+ completedEventResult: RuntimeTurnResult | null;
20
+ emittedContextCompactionKeys: Set<string>;
21
+ resolve: (value: RuntimeTurnResult) => void;
22
+ reject: (error: unknown) => void;
23
+ }
24
+
25
+ function ensureAgentMessageState(
26
+ turnStates: Map<string, RuntimeTurnState>,
27
+ turnId: string,
28
+ itemId: string,
29
+ ): AgentMessageState | null {
30
+ const turn = turnStates.get(turnId);
31
+ if (!turn || !itemId) {
32
+ return null;
33
+ }
34
+ let item = turn.agentMessages.get(itemId);
35
+ if (!item) {
36
+ item = {
37
+ order: turn.nextAgentMessageOrder++,
38
+ phase: 'unknown',
39
+ buffer: '',
40
+ completedText: null,
41
+ progressSentText: null,
42
+ };
43
+ turn.agentMessages.set(itemId, item);
44
+ }
45
+ return item;
46
+ }
47
+
48
+ export function noteAgentMessageItem(
49
+ turnStates: Map<string, RuntimeTurnState>,
50
+ turnId: string,
51
+ item: any,
52
+ ): void {
53
+ if (!item || item.type !== 'agentMessage' || typeof item.id !== 'string') {
54
+ return;
55
+ }
56
+ const state = ensureAgentMessageState(turnStates, turnId, item.id);
57
+ if (!state) {
58
+ return;
59
+ }
60
+ const phase = normalizeAgentMessagePhase(item.phase);
61
+ if (phase !== 'unknown' || state.phase === 'unknown') {
62
+ state.phase = phase;
63
+ }
64
+ if (typeof item.text === 'string') {
65
+ state.completedText = item.text;
66
+ }
67
+ }
68
+
69
+ export function consumeCompletedAgentMessageProgress(
70
+ turnStates: Map<string, RuntimeTurnState>,
71
+ turnId: string,
72
+ itemId: string,
73
+ ): { text: string; phase: AgentMessagePhase } | null {
74
+ const item = ensureAgentMessageState(turnStates, turnId, itemId);
75
+ if (!item) {
76
+ return null;
77
+ }
78
+ const text = String(item.completedText ?? item.buffer ?? '').trim();
79
+ if (!text) {
80
+ return null;
81
+ }
82
+ if (item.progressSentText === text) {
83
+ return null;
84
+ }
85
+ item.progressSentText = text;
86
+ return { text, phase: item.phase };
87
+ }
88
+
89
+ export function buildReplyFromThreadItems(items: Array<any>): string {
90
+ return items
91
+ .filter((item) => item?.type === 'agentMessage')
92
+ .map((item, index) => ({
93
+ order: index,
94
+ phase: normalizeAgentMessagePhase(item.phase),
95
+ text: typeof item.text === 'string' ? item.text : '',
96
+ }))
97
+ .sort((left, right) => left.order - right.order)
98
+ .filter((item) => item.phase !== 'commentary')
99
+ .map((item) => item.text)
100
+ .filter((item) => Boolean(item))
101
+ .join('');
102
+ }
103
+
104
+ export function observeContextCompactionEvent(
105
+ turnStates: Map<string, RuntimeTurnState>,
106
+ msg: JsonRpcMessage,
107
+ emitContextCompactionProgress: (turnId: string, itemId: string) => void,
108
+ ): boolean {
109
+ if (!msg.method) {
110
+ return false;
111
+ }
112
+ const params = msg.params || {};
113
+ const threadId = typeof params.threadId === 'string' ? params.threadId : null;
114
+ const turnId = typeof params.turnId === 'string' ? params.turnId : null;
115
+ if (!turnId || !turnStates.has(turnId)) {
116
+ return false;
117
+ }
118
+
119
+ if (msg.method === 'thread/compacted') {
120
+ const fallbackItemId = typeof params.itemId === 'string' ? params.itemId : 'thread_compacted';
121
+ logInfo('runtime', 'thread_compacted_event', {
122
+ threadId,
123
+ turnId,
124
+ itemId: fallbackItemId,
125
+ });
126
+ emitContextCompactionProgress(turnId, fallbackItemId);
127
+ return true;
128
+ }
129
+
130
+ if (msg.method !== 'item/started' && msg.method !== 'item/updated' && msg.method !== 'item/completed') {
131
+ return false;
132
+ }
133
+
134
+ const item = params.item;
135
+ const itemId = typeof params.itemId === 'string'
136
+ ? params.itemId
137
+ : (item && typeof item.id === 'string' ? item.id : null);
138
+ if (!itemId || !isContextCompactionType(item?.type)) {
139
+ return false;
140
+ }
141
+
142
+ logInfo('runtime', 'context_compaction_event', {
143
+ method: msg.method,
144
+ threadId,
145
+ turnId,
146
+ itemId,
147
+ });
148
+ emitContextCompactionProgress(turnId, itemId);
149
+ return true;
150
+ }