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,367 @@
1
+ import type { OutboundEnvelope } from './channel-types.ts';
2
+ import type { McpToolApprovalRule } from './config.ts';
3
+ import { logInfo, logWarn } from './logger.ts';
4
+ import { compactPreview } from './receiver-helpers.ts';
5
+ import { maybeAutoResolveRuntimeUserInput } from './runtime-user-input.ts';
6
+ import type { SessionStore } from './session-store.ts';
7
+ import { contextCompactionStatusMessage, userInputMessage, waitingApprovalStatusMessage, waitingUserInputStatusMessage, workingNoticeMessage } from './translator.ts';
8
+ import type { ApprovalRecord, ApprovalRequest, ConversationRef, InboundMessage, RuntimeProgressMeta, RuntimeThreadStatus, SessionMeta, UserInputRequest } from './types.ts';
9
+
10
+ interface SendLike {
11
+ (
12
+ outbound: OutboundEnvelope,
13
+ extra?: Record<string, unknown>,
14
+ delivery?: { trackTurnId?: string | null },
15
+ ): Promise<boolean>;
16
+ }
17
+
18
+ interface RuntimeResolutionLike {
19
+ resolveApproval(requestId: string | number, decision: 'accept' | 'acceptForSession' | 'decline' | 'cancel'): Promise<void>;
20
+ resolveUserInput(requestId: string | number, response: { answers: Record<string, { answers: string[] }> }): Promise<void>;
21
+ resolveElicitation(
22
+ requestId: string | number,
23
+ response: { action: 'accept' | 'decline' | 'cancel'; content: unknown | null },
24
+ ): Promise<void>;
25
+ }
26
+
27
+ interface RuntimeCallbackState {
28
+ getCurrentTurnId(): string | null;
29
+ setCurrentTurnId(turnId: string): void;
30
+ setLastProgressAt(timestamp: number): void;
31
+ waitingApprovalNoticeState: { sent: boolean };
32
+ isWaitingUserInputNoticeSent(): boolean;
33
+ markWaitingUserInputNoticeSent(): void;
34
+ contextCompactionNoticeKeys: Set<string>;
35
+ }
36
+
37
+ interface RuntimeCallbackDeps {
38
+ sessionStore: SessionStore;
39
+ runtime: RuntimeResolutionLike;
40
+ send: SendLike;
41
+ mcpToolApprovalRules: McpToolApprovalRule[];
42
+ clearRuntimeRecovery(ref: ConversationRef): void;
43
+ isTurnStillCurrent(ref: ConversationRef, messageId: string, turnId: string | null): boolean;
44
+ isBlockingTurnStillRelevant(ref: ConversationRef, threadId: string | null, turnId: string | null): boolean;
45
+ noteStaleTurnSuppressed(
46
+ ref: ConversationRef,
47
+ details: {
48
+ messageId: string;
49
+ threadId: string | null;
50
+ turnId: string | null;
51
+ stage: 'progress' | 'approval' | 'user_input';
52
+ reason: string;
53
+ status?: string | null;
54
+ },
55
+ ): void;
56
+ shouldForwardProgressUpdate(text: string, meta?: RuntimeProgressMeta): boolean;
57
+ evaluateApprovalAutonomy(approval: ApprovalRecord): {
58
+ action: 'accept' | 'decline';
59
+ reason: string;
60
+ policy: string;
61
+ matchedRule?: string;
62
+ ruleSource?: string;
63
+ commandForm?: string;
64
+ } | null;
65
+ scheduleApprovalNotice(
66
+ conversationRef: ConversationRef,
67
+ threadId: string,
68
+ turnId: string,
69
+ noticeState: { sent: boolean },
70
+ ): void;
71
+ }
72
+
73
+ interface RuntimeCallbackContext {
74
+ message: InboundMessage;
75
+ threadId: string;
76
+ turnEntryMessageId: string;
77
+ fallbackSession: SessionMeta;
78
+ state: RuntimeCallbackState;
79
+ }
80
+
81
+ export function createInboundRuntimeCallbacks(
82
+ deps: RuntimeCallbackDeps,
83
+ context: RuntimeCallbackContext,
84
+ ): {
85
+ onTurnStarted: (turnId: string) => Promise<void>;
86
+ onProgress: (text: string, meta?: RuntimeProgressMeta) => Promise<void>;
87
+ onThreadStatus: (status: RuntimeThreadStatus, meta?: { threadId?: string | null; turnId?: string | null }) => Promise<void>;
88
+ onApproval: (approval: ApprovalRequest) => Promise<void>;
89
+ onUserInput: (request: UserInputRequest) => Promise<void>;
90
+ } {
91
+ return {
92
+ onTurnStarted: async (turnId) => {
93
+ context.state.setCurrentTurnId(turnId);
94
+ deps.clearRuntimeRecovery(context.message.conversationRef);
95
+ deps.sessionStore.markActiveTurn(context.message.conversationRef, turnId, {
96
+ messageId: context.message.messageId,
97
+ promptPreview: context.message.text,
98
+ });
99
+ logInfo('receiver', 'turn_started', {
100
+ messageId: context.message.messageId,
101
+ conversationId: context.message.conversationRef.conversationId,
102
+ threadId: context.threadId,
103
+ turnId,
104
+ });
105
+ },
106
+ onProgress: async (text, meta) => {
107
+ const currentTurnId = context.state.getCurrentTurnId();
108
+ if (currentTurnId && !deps.isTurnStillCurrent(context.message.conversationRef, context.turnEntryMessageId, currentTurnId)) {
109
+ deps.noteStaleTurnSuppressed(context.message.conversationRef, {
110
+ messageId: context.message.messageId,
111
+ threadId: context.threadId,
112
+ turnId: currentTurnId,
113
+ stage: 'progress',
114
+ reason: 'session advanced to a newer inbound message',
115
+ });
116
+ return;
117
+ }
118
+ context.state.setLastProgressAt(Date.now());
119
+ deps.clearRuntimeRecovery(context.message.conversationRef);
120
+ if (currentTurnId) {
121
+ deps.sessionStore.markTurnRunning(context.message.conversationRef, currentTurnId);
122
+ }
123
+ const rawText = String(text || '');
124
+ const preview = compactPreview(rawText, 500);
125
+ if (preview) {
126
+ logInfo('receiver', 'runtime_progress', {
127
+ messageId: context.message.messageId,
128
+ conversationId: context.message.conversationRef.conversationId,
129
+ threadId: context.threadId,
130
+ turnId: currentTurnId,
131
+ progressKind: meta?.kind || null,
132
+ progressPhase: meta?.phase || null,
133
+ itemId: meta?.itemId || null,
134
+ preview: compactPreview(rawText, 160),
135
+ });
136
+ }
137
+ if (meta?.kind === 'context_compaction') {
138
+ const dedupeKey = `${context.threadId}:${currentTurnId || meta?.turnId || 'unknown'}:${meta?.itemId || 'unknown'}`;
139
+ if (context.state.contextCompactionNoticeKeys.has(dedupeKey)) {
140
+ return;
141
+ }
142
+ context.state.contextCompactionNoticeKeys.add(dedupeKey);
143
+ await deps.send({
144
+ type: 'status_reply',
145
+ conversationRef: context.message.conversationRef,
146
+ text: contextCompactionStatusMessage(),
147
+ }, {
148
+ source: 'runtime_context_compaction',
149
+ thread_id: context.threadId,
150
+ turn_id: currentTurnId,
151
+ progress_kind: meta?.kind || null,
152
+ progress_phase: meta?.phase || null,
153
+ item_id: meta?.itemId || null,
154
+ });
155
+ return;
156
+ }
157
+ if (!deps.shouldForwardProgressUpdate(rawText, meta)) {
158
+ return;
159
+ }
160
+ const outboundText = workingNoticeMessage(rawText);
161
+ if (!outboundText) {
162
+ return;
163
+ }
164
+ await deps.send({
165
+ type: 'progress_update',
166
+ conversationRef: context.message.conversationRef,
167
+ text: outboundText,
168
+ }, {
169
+ source: 'runtime_progress',
170
+ thread_id: context.threadId,
171
+ turn_id: currentTurnId,
172
+ progress_kind: meta?.kind || null,
173
+ progress_phase: meta?.phase || null,
174
+ item_id: meta?.itemId || null,
175
+ });
176
+ },
177
+ onThreadStatus: async (_status, meta) => {
178
+ const turnId = meta?.turnId || context.state.getCurrentTurnId();
179
+ if (!turnId) {
180
+ return;
181
+ }
182
+ if (!deps.isTurnStillCurrent(context.message.conversationRef, context.turnEntryMessageId, turnId)) {
183
+ return;
184
+ }
185
+ const latestSession = deps.sessionStore.getSession(context.message.conversationRef) || context.fallbackSession;
186
+ if (latestSession.pendingApprovalIds.length > 0) {
187
+ if (context.state.waitingApprovalNoticeState.sent) {
188
+ return;
189
+ }
190
+ context.state.waitingApprovalNoticeState.sent = true;
191
+ await deps.send({
192
+ type: 'status_reply',
193
+ conversationRef: context.message.conversationRef,
194
+ text: waitingApprovalStatusMessage(),
195
+ }, {
196
+ source: 'waiting_approval',
197
+ thread_id: meta?.threadId || context.threadId,
198
+ turn_id: turnId,
199
+ });
200
+ return;
201
+ }
202
+ if (!latestSession.pendingUserInputRequestId) {
203
+ return;
204
+ }
205
+ if (context.state.isWaitingUserInputNoticeSent()) {
206
+ return;
207
+ }
208
+ context.state.markWaitingUserInputNoticeSent();
209
+ const pendingInput = deps.sessionStore.getPendingUserInput(context.message.conversationRef) || {
210
+ questions: [],
211
+ elicitation: null,
212
+ };
213
+ await deps.send({
214
+ type: 'status_reply',
215
+ conversationRef: context.message.conversationRef,
216
+ text: waitingUserInputStatusMessage(pendingInput),
217
+ }, {
218
+ source: 'waiting_user_input',
219
+ thread_id: meta?.threadId || context.threadId,
220
+ turn_id: turnId,
221
+ });
222
+ },
223
+ onApproval: async (approval) => {
224
+ const existingApproval = deps.sessionStore.getApproval(context.message.conversationRef, approval.approvalId);
225
+ if (existingApproval && existingApproval.status !== 'pending') {
226
+ await deps.runtime.resolveApproval(
227
+ approval.requestId,
228
+ existingApproval.status === 'accepted' ? 'accept' : existingApproval.status === 'declined' ? 'decline' : 'cancel',
229
+ );
230
+ return;
231
+ }
232
+ if (!deps.isTurnStillCurrent(context.message.conversationRef, context.turnEntryMessageId, approval.turnId)
233
+ && !deps.isBlockingTurnStillRelevant(context.message.conversationRef, approval.threadId, approval.turnId)) {
234
+ deps.noteStaleTurnSuppressed(context.message.conversationRef, {
235
+ messageId: context.message.messageId,
236
+ threadId: approval.threadId,
237
+ turnId: approval.turnId,
238
+ stage: 'approval',
239
+ reason: 'session advanced before approval request reached the user',
240
+ });
241
+ await deps.runtime.resolveApproval(approval.requestId, 'cancel');
242
+ return;
243
+ }
244
+ deps.clearRuntimeRecovery(context.message.conversationRef);
245
+ logWarn('receiver', 'approval_requested', {
246
+ approvalId: approval.approvalId,
247
+ kind: approval.kind,
248
+ threadId: approval.threadId,
249
+ turnId: approval.turnId,
250
+ });
251
+ const latestSession = deps.sessionStore.getSession(context.message.conversationRef);
252
+ if (existingApproval?.status === 'pending' && latestSession?.pendingApprovalIds.includes(approval.approvalId)) {
253
+ return;
254
+ }
255
+ const record: ApprovalRecord = {
256
+ approvalId: approval.approvalId,
257
+ kind: approval.kind,
258
+ requestId: approval.requestId,
259
+ threadId: approval.threadId,
260
+ turnId: approval.turnId,
261
+ messageId: approval.messageId,
262
+ command: approval.command,
263
+ reason: approval.reason,
264
+ grantRoot: approval.grantRoot,
265
+ cwd: approval.cwd,
266
+ createdAt: new Date().toISOString(),
267
+ status: 'pending',
268
+ visible: false,
269
+ };
270
+ deps.sessionStore.recordApproval(context.message.conversationRef, record, { visible: false });
271
+ const autonomyDecision = deps.evaluateApprovalAutonomy(record);
272
+ if (autonomyDecision) {
273
+ await deps.runtime.resolveApproval(approval.requestId, autonomyDecision.action === 'accept' ? 'accept' : 'decline');
274
+ deps.sessionStore.resolveApproval(
275
+ context.message.conversationRef,
276
+ approval.approvalId,
277
+ autonomyDecision.action === 'accept' ? 'accepted' : 'declined',
278
+ );
279
+ return;
280
+ }
281
+ deps.scheduleApprovalNotice(
282
+ context.message.conversationRef,
283
+ approval.threadId,
284
+ approval.turnId,
285
+ context.state.waitingApprovalNoticeState,
286
+ );
287
+ },
288
+ onUserInput: async (request) => {
289
+ const currentPendingInput = deps.sessionStore.getPendingUserInput(context.message.conversationRef);
290
+ const latestSession = deps.sessionStore.getSession(context.message.conversationRef);
291
+ if (currentPendingInput?.requestId === request.requestId
292
+ && latestSession?.pendingUserInputRequestId === String(request.requestId)) {
293
+ return;
294
+ }
295
+ if (!deps.isTurnStillCurrent(context.message.conversationRef, context.turnEntryMessageId, request.turnId)
296
+ && !deps.isBlockingTurnStillRelevant(context.message.conversationRef, request.threadId, request.turnId)) {
297
+ deps.noteStaleTurnSuppressed(context.message.conversationRef, {
298
+ messageId: context.message.messageId,
299
+ threadId: request.threadId,
300
+ turnId: request.turnId,
301
+ stage: 'user_input',
302
+ reason: 'session advanced before user input request reached the user',
303
+ });
304
+ if (request.kind === 'mcp_elicitation') {
305
+ await deps.runtime.resolveElicitation(request.requestId, { action: 'cancel', content: null });
306
+ } else {
307
+ await deps.runtime.resolveUserInput(request.requestId, { answers: {} });
308
+ }
309
+ return;
310
+ }
311
+ deps.clearRuntimeRecovery(context.message.conversationRef);
312
+ const autoResolution = maybeAutoResolveRuntimeUserInput(request, deps.mcpToolApprovalRules);
313
+ if (autoResolution) {
314
+ logInfo('receiver', 'runtime_user_input_auto_resolved', {
315
+ conversationId: context.message.conversationRef.conversationId,
316
+ requestId: request.requestId,
317
+ kind: request.kind || 'questionnaire',
318
+ serverName: autoResolution.serverName,
319
+ toolName: autoResolution.toolName,
320
+ answerLabel: autoResolution.answerLabel,
321
+ matchedRule: autoResolution.matchedRule,
322
+ });
323
+ if (autoResolution.elicitationResponse) {
324
+ await deps.runtime.resolveElicitation(request.requestId, autoResolution.elicitationResponse);
325
+ } else if (autoResolution.answers) {
326
+ await deps.runtime.resolveUserInput(request.requestId, { answers: autoResolution.answers });
327
+ }
328
+ return;
329
+ }
330
+
331
+ const pending = deps.sessionStore.notePendingUserInput(
332
+ context.message.conversationRef,
333
+ request,
334
+ { visible: false },
335
+ );
336
+ const delivered = await deps.send({
337
+ type: 'user_input_requested',
338
+ conversationRef: context.message.conversationRef,
339
+ request: pending,
340
+ text: userInputMessage(pending),
341
+ }, {
342
+ source: 'user_input_requested',
343
+ thread_id: request.threadId,
344
+ turn_id: request.turnId,
345
+ request_id: request.requestId,
346
+ });
347
+ if (!delivered) {
348
+ return;
349
+ }
350
+ const visiblePending = deps.sessionStore.markPendingUserInputVisible(context.message.conversationRef, request.requestId) || pending;
351
+ if (context.state.isWaitingUserInputNoticeSent()) {
352
+ return;
353
+ }
354
+ context.state.markWaitingUserInputNoticeSent();
355
+ await deps.send({
356
+ type: 'status_reply',
357
+ conversationRef: context.message.conversationRef,
358
+ text: waitingUserInputStatusMessage(visiblePending),
359
+ }, {
360
+ source: 'waiting_user_input',
361
+ thread_id: request.threadId,
362
+ turn_id: request.turnId,
363
+ request_id: request.requestId,
364
+ });
365
+ },
366
+ };
367
+ }
@@ -0,0 +1,132 @@
1
+ import type { WorkAllyConfig } from './config.ts';
2
+ import { isRuntimeFinalStateUnconfirmedError, isRuntimeInfrastructureError } from './receiver-recovery.ts';
3
+ import { runtimeFinalStateRecoveryRequiredMessage, runtimeInfrastructureErrorMessage, systemNoticeMessage } from './translator.ts';
4
+ import type { InboundMessage, RuntimeProgressMeta } from './types.ts';
5
+
6
+ export function buildPromptBody(message: InboundMessage): string {
7
+ const current = message.text.trim();
8
+ const sourceMessageType = String(message.sourceMessageType || '').trim();
9
+ const replyToText = String(message.replyToText || '').trim();
10
+ const sourceContext = sourceMessageType === 'merge_forward'
11
+ ? [
12
+ 'Inbound source: Feishu forwarded chat record (`merge_forward`).',
13
+ 'Treat the following body as forwarded conversation content, not as plain user-authored single-message input.',
14
+ '',
15
+ ]
16
+ : [];
17
+
18
+ if (!replyToText) {
19
+ return [...sourceContext, current].join('\n');
20
+ }
21
+
22
+ return [
23
+ ...sourceContext,
24
+ 'User is replying to a previous Feishu message.',
25
+ '',
26
+ `ReplyToMessageId: ${message.replyToMessageId || 'unknown'}`,
27
+ message.rootMessageId ? `RootMessageId: ${message.rootMessageId}` : null,
28
+ '',
29
+ 'Replied message body:',
30
+ replyToText,
31
+ '',
32
+ 'Current user message:',
33
+ current,
34
+ ].filter(Boolean).join('\n');
35
+ }
36
+
37
+ export function buildPrompt(message: InboundMessage): string {
38
+ const body = buildPromptBody(message);
39
+ if (message.conversationRef.chatType !== 'group') {
40
+ return body;
41
+ }
42
+
43
+ const senderName = String(message.senderName || '').trim();
44
+ const senderLabel = senderName || message.senderId;
45
+ return [
46
+ 'Channel: Feishu',
47
+ 'Chat type: group',
48
+ `Sender: ${senderLabel}`,
49
+ `Sender ID: ${message.senderId}`,
50
+ '',
51
+ 'Message:',
52
+ body,
53
+ ].join('\n');
54
+ }
55
+
56
+ export function interactiveSandboxPolicy(config: WorkAllyConfig): unknown {
57
+ const mode = config.runtime.sandboxMode;
58
+ if (mode === 'danger-full-access') {
59
+ return { type: 'dangerFullAccess' as const };
60
+ }
61
+ if (mode === 'read-only') {
62
+ return {
63
+ type: 'readOnly' as const,
64
+ networkAccess: true,
65
+ access: {
66
+ type: 'restricted' as const,
67
+ includePlatformDefaults: true,
68
+ readableRoots: [config.workspaceRoot],
69
+ },
70
+ };
71
+ }
72
+ const writableRoots = [config.workspaceRoot];
73
+ const assistantHome = config.assistant?.assistantHome;
74
+ if (assistantHome && !writableRoots.includes(assistantHome)) {
75
+ writableRoots.push(assistantHome);
76
+ }
77
+ return {
78
+ type: 'workspaceWrite' as const,
79
+ networkAccess: true,
80
+ writableRoots,
81
+ readOnlyAccess: {
82
+ type: 'restricted' as const,
83
+ includePlatformDefaults: true,
84
+ readableRoots: writableRoots,
85
+ },
86
+ };
87
+ }
88
+
89
+ export function heartbeatMs(config: WorkAllyConfig): number {
90
+ const seconds = Number(config.progressHeartbeatSeconds || 0);
91
+ if (!Number.isFinite(seconds) || seconds <= 0) {
92
+ return 0;
93
+ }
94
+ return Math.max(1, Math.round(seconds * 1000));
95
+ }
96
+
97
+ export function shouldForwardProgressUpdate(text: string, meta?: RuntimeProgressMeta): boolean {
98
+ const normalized = String(text || '').trim();
99
+ if (!meta?.kind) {
100
+ return !['正在处理...', '正在处理,请稍候。', '正在思考中,请稍候…'].includes(normalized);
101
+ }
102
+ if (meta.kind !== 'agent_message') {
103
+ return false;
104
+ }
105
+ return meta.phase !== 'final_answer';
106
+ }
107
+
108
+ export function isMissingThreadError(error: unknown): boolean {
109
+ const code = (error && typeof error === 'object' && 'code' in error) ? String((error as { code?: unknown }).code) : '';
110
+ const message = error instanceof Error
111
+ ? error.message
112
+ : (error && typeof error === 'object' && 'message' in error ? String((error as { message?: unknown }).message || '') : String(error || ''));
113
+ return code === '-32600' && /thread not found|no rollout found for thread id/i.test(message);
114
+ }
115
+
116
+ export function isStaleTurnError(error: unknown): boolean {
117
+ const message = error instanceof Error
118
+ ? error.message
119
+ : (error && typeof error === 'object' && 'message' in error ? String((error as { message?: unknown }).message || '') : String(error || ''));
120
+ return /turn not active|turn not found|unknown turn|turn interrupted/i.test(message);
121
+ }
122
+
123
+ export function normalizeErrorReplyText(text: string): string {
124
+ const resolved = String(text || '').trim() || '处理失败。';
125
+ if (isRuntimeFinalStateUnconfirmedError(resolved)) {
126
+ return systemNoticeMessage(runtimeFinalStateRecoveryRequiredMessage());
127
+ }
128
+ if (isRuntimeInfrastructureError(resolved)) {
129
+ return systemNoticeMessage(runtimeInfrastructureErrorMessage());
130
+ }
131
+ return resolved;
132
+ }
@@ -0,0 +1,124 @@
1
+ import { logWarn } from './logger.ts';
2
+ import { connectionLifecycleMessage, systemNoticeMessage } from './translator.ts';
3
+ import type { OutboundEnvelope } from './channel-types.ts';
4
+ import type { SessionStore } from './session-store.ts';
5
+ import type { ConnectionLifecycleEvent, ConversationRef, RuntimeThreadStatus, ServerRequestResolvedEvent, SessionMeta } from './types.ts';
6
+
7
+ interface RuntimeRecoveryRegistry {
8
+ isActive(conversationRef: ConversationRef): boolean;
9
+ mark(conversationRef: ConversationRef): void;
10
+ clear(conversationRef: ConversationRef): void;
11
+ }
12
+
13
+ interface RuntimeThreadStatusReader {
14
+ readThreadStatus(threadId: string): Promise<RuntimeThreadStatus | null>;
15
+ }
16
+
17
+ interface ConnectionLifecycleDeps {
18
+ sessionStore: Pick<SessionStore, 'listSessions' | 'hasActiveTurn' | 'resolveSessionKey'>;
19
+ runtimeRecovery: RuntimeRecoveryRegistry;
20
+ send: (outbound: OutboundEnvelope, extra?: Record<string, unknown>) => Promise<boolean>;
21
+ }
22
+
23
+ interface ServerRequestResolvedDeps {
24
+ sessionStore: Pick<SessionStore, 'findApprovalByRequestId' | 'resolveApproval' | 'findPendingUserInputByRequestId' | 'clearPendingUserInput'>;
25
+ }
26
+
27
+ function latestFeishuConversationRef(sessions: SessionMeta[]): ConversationRef | null {
28
+ const prioritized = sessions
29
+ .filter((session) => session.conversationRef.channel === 'feishu')
30
+ .sort((left, right) => {
31
+ const activeDelta = Number(Boolean(right.activeTurnId)) - Number(Boolean(left.activeTurnId));
32
+ if (activeDelta !== 0) {
33
+ return activeDelta;
34
+ }
35
+ return String(right.updatedAt).localeCompare(String(left.updatedAt));
36
+ });
37
+ return prioritized[0]?.conversationRef || null;
38
+ }
39
+
40
+ export function createRuntimeRecoveryRegistry(
41
+ sessionStore: Pick<SessionStore, 'resolveSessionKey'>,
42
+ ): RuntimeRecoveryRegistry {
43
+ const activeSessionKeys = new Set<string>();
44
+ const sessionKeyFor = (conversationRef: ConversationRef): string => sessionStore.resolveSessionKey(conversationRef);
45
+ return {
46
+ isActive: (conversationRef) => activeSessionKeys.has(sessionKeyFor(conversationRef)),
47
+ mark: (conversationRef) => {
48
+ activeSessionKeys.add(sessionKeyFor(conversationRef));
49
+ },
50
+ clear: (conversationRef) => {
51
+ activeSessionKeys.delete(sessionKeyFor(conversationRef));
52
+ },
53
+ };
54
+ }
55
+
56
+ export async function readCurrentRuntimeThreadStatus(
57
+ deps: { runtime: RuntimeThreadStatusReader },
58
+ session: { threadId: string | null },
59
+ ): Promise<RuntimeThreadStatus | null> {
60
+ if (!session.threadId) {
61
+ return null;
62
+ }
63
+ try {
64
+ return await deps.runtime.readThreadStatus(session.threadId);
65
+ } catch (error) {
66
+ logWarn('receiver', 'runtime_thread_status_read_failed', {
67
+ threadId: session.threadId,
68
+ error: error instanceof Error ? (error.stack || error.message) : String(error),
69
+ });
70
+ return null;
71
+ }
72
+ }
73
+
74
+ export async function handleConnectionLifecycleEvent(
75
+ deps: ConnectionLifecycleDeps,
76
+ event: ConnectionLifecycleEvent,
77
+ ): Promise<void> {
78
+ const conversationRef = latestFeishuConversationRef(deps.sessionStore.listSessions());
79
+ if (!conversationRef) {
80
+ return;
81
+ }
82
+
83
+ const hasActiveTurn = event.hasActiveTurn ?? deps.sessionStore.hasActiveTurn(conversationRef);
84
+ const activeRuntimeRecovery = event.source === 'runtime' && hasActiveTurn;
85
+ if (activeRuntimeRecovery) {
86
+ deps.runtimeRecovery.mark(conversationRef);
87
+ } else if (event.source === 'runtime' && event.state === 'reconnected') {
88
+ deps.runtimeRecovery.clear(conversationRef);
89
+ }
90
+
91
+ const suppressed = activeRuntimeRecovery && event.state === 'reconnecting';
92
+
93
+ if (event.source === 'runtime' || suppressed) {
94
+ return;
95
+ }
96
+
97
+ const text = systemNoticeMessage(connectionLifecycleMessage(event, { hasActiveTurn }));
98
+ await deps.send({
99
+ type: 'progress_update',
100
+ conversationRef,
101
+ text,
102
+ }, {
103
+ source: 'connection_lifecycle',
104
+ connection_source: event.source,
105
+ connection_state: event.state,
106
+ runtime_recovery_active: activeRuntimeRecovery,
107
+ has_active_turn: hasActiveTurn,
108
+ });
109
+ }
110
+
111
+ export function handleServerRequestResolvedEvent(
112
+ deps: ServerRequestResolvedDeps,
113
+ event: ServerRequestResolvedEvent,
114
+ ): void {
115
+ const approvalMatch = deps.sessionStore.findApprovalByRequestId(event.requestId);
116
+ if (approvalMatch && approvalMatch.approval.status === 'pending') {
117
+ deps.sessionStore.resolveApproval(approvalMatch.meta.conversationRef, approvalMatch.approval.approvalId, 'cancelled');
118
+ }
119
+
120
+ const userInputMatch = deps.sessionStore.findPendingUserInputByRequestId(event.requestId);
121
+ if (userInputMatch && userInputMatch.request.status === 'pending') {
122
+ deps.sessionStore.clearPendingUserInput(userInputMatch.meta.conversationRef, { requestId: event.requestId });
123
+ }
124
+ }