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,189 @@
1
+ import type { CommandApprovalRuleset } from './approval-rules.ts';
2
+ import { APPROVAL_BATCH_WINDOW_MS, TURN_RECOVERY_VISIBILITY_DELAY_MS, errorDetailsFromUnknown } from './receiver-helpers.ts';
3
+ import { markFinalReplyDelivered, recordPendingFinalReplyDelivery, redeliverPendingFinalReplies, redeliverPendingTurnResult } from './receiver-delivery.ts';
4
+ import { flushApprovalNotice, maybeHandleWaitingApprovalShortcut, maybeResumePendingUserInput } from './receiver-human-gate.ts';
5
+ import { isRuntimeInfrastructureError, sendRuntimeRecoveryNotice, tryRecoverTurnResult } from './receiver-recovery.ts';
6
+ import { approvalBoundaryReason, evaluateApprovalAutonomy } from './receiver-approval-autonomy.ts';
7
+ import { normalizeErrorReplyText } from './receiver-runtime-policy.ts';
8
+ import { handleConnectionLifecycleEvent, handleServerRequestResolvedEvent } from './receiver-runtime-state.ts';
9
+ import { logWarn } from './logger.ts';
10
+ import type { WorkAllyConfig } from './config.ts';
11
+ import type { SessionStore } from './session-store.ts';
12
+ import type { ConnectionLifecycleEvent, ConversationRef, InboundMessage, RuntimeProgressMeta, RuntimeThreadStatus, RuntimeTurnResult, ServerRequestResolvedEvent, UserInputRequest } from './types.ts';
13
+
14
+ interface SendLike {
15
+ (outbound: any, extra?: Record<string, unknown>, delivery?: { trackTurnId?: string | null }): 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
+ recoverTurnResult?(threadId: string, turnId: string, options?: { timeoutMs?: number; pollMs?: number }): Promise<RuntimeTurnResult | null>;
26
+ }
27
+
28
+ interface RuntimeRecoveryRegistry {
29
+ clear(conversationRef: ConversationRef): void;
30
+ mark(conversationRef: ConversationRef): void;
31
+ isActive(conversationRef: ConversationRef): boolean;
32
+ }
33
+
34
+ interface ReceiverSupportActionDeps {
35
+ config: WorkAllyConfig;
36
+ sessionStore: SessionStore;
37
+ runtime: RuntimeResolutionLike;
38
+ runtimeRecovery: RuntimeRecoveryRegistry;
39
+ approvalNoticeTimers: Map<string, NodeJS.Timeout>;
40
+ commandApprovalRules: CommandApprovalRuleset;
41
+ send: SendLike;
42
+ }
43
+
44
+ export function createReceiverSupportActions(deps: ReceiverSupportActionDeps) {
45
+ const approvalAutonomyDeps = () => ({
46
+ workspaceRoot: deps.config.workspaceRoot,
47
+ assistantHome: deps.config.assistant?.assistantHome || deps.config.paths.assistantHome || null,
48
+ commandApprovalRules: deps.commandApprovalRules,
49
+ });
50
+
51
+ const recoveryAttemptVisibilityDelayMs = (): number => {
52
+ const raw = Number(process.env.WORK_ALLY_TURN_RECOVERY_VISIBILITY_DELAY_MS || String(TURN_RECOVERY_VISIBILITY_DELAY_MS));
53
+ return Number.isFinite(raw) ? Math.max(0, raw) : TURN_RECOVERY_VISIBILITY_DELAY_MS;
54
+ };
55
+
56
+ const flushScheduledApprovalNotice = async (
57
+ conversationRef: InboundMessage['conversationRef'],
58
+ threadId: string,
59
+ turnId: string,
60
+ noticeState: { sent: boolean },
61
+ ): Promise<void> => flushApprovalNotice({
62
+ sessionStore: deps.sessionStore,
63
+ send: deps.send,
64
+ approvalBoundaryReason: (approval) => approvalBoundaryReason(approvalAutonomyDeps(), approval),
65
+ }, conversationRef, threadId, turnId, noticeState);
66
+
67
+ return {
68
+ handleConnectionLifecycle: async (event: ConnectionLifecycleEvent): Promise<void> => {
69
+ await handleConnectionLifecycleEvent({
70
+ sessionStore: deps.sessionStore,
71
+ runtimeRecovery: deps.runtimeRecovery,
72
+ send: deps.send,
73
+ }, event);
74
+ },
75
+
76
+ handleServerRequestResolved: (event: ServerRequestResolvedEvent): void => {
77
+ handleServerRequestResolvedEvent({
78
+ sessionStore: deps.sessionStore,
79
+ }, event);
80
+ },
81
+
82
+ sendRuntimeRecoveryNotice: async (
83
+ conversationRef: InboundMessage['conversationRef'],
84
+ threadId: string,
85
+ turnId: string | null,
86
+ errorText: string,
87
+ recoveryKind: 'infrastructure' | 'final_state_unconfirmed',
88
+ ): Promise<void> => sendRuntimeRecoveryNotice({
89
+ clearRuntimeRecovery: (ref) => deps.runtimeRecovery.clear(ref),
90
+ send: deps.send,
91
+ }, conversationRef, threadId, turnId, errorText, recoveryKind),
92
+
93
+ recordPendingFinalReplyDelivery: (
94
+ conversationRef: InboundMessage['conversationRef'],
95
+ threadId: string,
96
+ turnId: string,
97
+ text: string,
98
+ ): string => recordPendingFinalReplyDelivery({
99
+ sessionStore: deps.sessionStore,
100
+ }, conversationRef, threadId, turnId, text),
101
+
102
+ markFinalReplyDelivered: (conversationRef: InboundMessage['conversationRef'], threadId: string, turnId: string): void => {
103
+ markFinalReplyDelivered({
104
+ sessionStore: deps.sessionStore,
105
+ }, conversationRef, threadId, turnId);
106
+ },
107
+
108
+ redeliverPendingFinalReplies: async (conversationRef: InboundMessage['conversationRef']): Promise<number> => redeliverPendingFinalReplies({
109
+ sessionStore: deps.sessionStore,
110
+ send: deps.send,
111
+ }, conversationRef),
112
+
113
+ redeliverPendingTurnResult: async (
114
+ conversationRef: InboundMessage['conversationRef'],
115
+ ledgerTurnId: string,
116
+ ): Promise<boolean> => redeliverPendingTurnResult({
117
+ sessionStore: deps.sessionStore,
118
+ send: deps.send,
119
+ isRuntimeInfrastructureError: (text) => isRuntimeInfrastructureError(text),
120
+ normalizeErrorReplyText: (text) => normalizeErrorReplyText(text),
121
+ }, conversationRef, ledgerTurnId),
122
+
123
+ tryRecoverTurnResult: async (
124
+ conversationRef: InboundMessage['conversationRef'],
125
+ threadId: string,
126
+ turnId: string | null,
127
+ messageId: string,
128
+ errorText: string,
129
+ startedAt: string | null,
130
+ callbacks: {
131
+ isTurnStillCurrent(conversationRef: ConversationRef, messageId: string, turnId: string | null): boolean;
132
+ markTurnSuppressed(conversationRef: ConversationRef, turnId: string | null, threadId: string | null, reason: string): void;
133
+ },
134
+ ): Promise<RuntimeTurnResult | null> => tryRecoverTurnResult({
135
+ runtime: deps.runtime,
136
+ clearRuntimeRecovery: (ref) => deps.runtimeRecovery.clear(ref),
137
+ markRuntimeRecovery: (ref) => deps.runtimeRecovery.mark(ref),
138
+ isTurnStillCurrent: callbacks.isTurnStillCurrent,
139
+ markTurnSuppressed: callbacks.markTurnSuppressed,
140
+ visibilityDelayMs: recoveryAttemptVisibilityDelayMs(),
141
+ }, conversationRef, threadId, turnId, messageId, errorText, startedAt),
142
+
143
+ scheduleApprovalNotice: (
144
+ conversationRef: InboundMessage['conversationRef'],
145
+ threadId: string,
146
+ turnId: string,
147
+ noticeState: { sent: boolean },
148
+ ): void => {
149
+ const sessionKey = deps.sessionStore.resolveSessionKey(conversationRef);
150
+ const existing = deps.approvalNoticeTimers.get(sessionKey);
151
+ if (existing) {
152
+ clearTimeout(existing);
153
+ }
154
+ const timer = setTimeout(() => {
155
+ deps.approvalNoticeTimers.delete(sessionKey);
156
+ void flushScheduledApprovalNotice(conversationRef, threadId, turnId, noticeState).catch((error) => {
157
+ logWarn('receiver', 'approval_notice_flush_failed', {
158
+ conversationId: conversationRef.conversationId,
159
+ threadId,
160
+ turnId,
161
+ error: errorDetailsFromUnknown(error),
162
+ });
163
+ });
164
+ }, APPROVAL_BATCH_WINDOW_MS);
165
+ timer.unref?.();
166
+ deps.approvalNoticeTimers.set(sessionKey, timer);
167
+ },
168
+
169
+ maybeHandleWaitingApprovalShortcut: async (
170
+ message: InboundMessage,
171
+ session: any,
172
+ ): Promise<boolean> => maybeHandleWaitingApprovalShortcut({
173
+ sessionStore: deps.sessionStore,
174
+ runtime: deps.runtime,
175
+ send: deps.send,
176
+ reactEmoji: deps.config.feishu.reactEmoji,
177
+ }, message, session),
178
+
179
+ maybeResumePendingUserInput: async (message: InboundMessage): Promise<boolean> => maybeResumePendingUserInput({
180
+ sessionStore: deps.sessionStore,
181
+ runtime: deps.runtime,
182
+ send: deps.send,
183
+ reactEmoji: deps.config.feishu.reactEmoji,
184
+ }, message),
185
+
186
+ approvalBoundaryReason: (approval: any) => approvalBoundaryReason(approvalAutonomyDeps(), approval),
187
+ evaluateApprovalAutonomy: (approval: any) => evaluateApprovalAutonomy(approvalAutonomyDeps(), approval),
188
+ };
189
+ }
@@ -0,0 +1,57 @@
1
+ import { logInfo } from './logger.ts';
2
+ import type { OutboundEnvelope } from './channel-types.ts';
3
+ import type { WorkAllyConfig } from './config.ts';
4
+ import type { SessionStore } from './session-store.ts';
5
+ import type { ConversationRef, InboundMessage } from './types.ts';
6
+
7
+ interface StartRuntimeLike {
8
+ startThread(cwd: string): Promise<{ id: string }>;
9
+ resumeThread(threadId: string, cwd: string): Promise<{ id: string }>;
10
+ }
11
+
12
+ interface SendLike {
13
+ (
14
+ outbound: OutboundEnvelope,
15
+ extra?: Record<string, unknown>,
16
+ delivery?: { trackTurnId?: string | null },
17
+ ): Promise<boolean>;
18
+ }
19
+
20
+ interface PrepareInboundThreadStartDeps {
21
+ config: Pick<WorkAllyConfig, 'workspaceRoot' | 'feishu'>;
22
+ runtime: StartRuntimeLike;
23
+ sessionStore: SessionStore;
24
+ send: SendLike;
25
+ clearRuntimeRecovery(conversationRef: ConversationRef): void;
26
+ }
27
+
28
+ export async function prepareInboundThreadStart(
29
+ deps: PrepareInboundThreadStartDeps,
30
+ message: InboundMessage,
31
+ ): Promise<{ id: string }> {
32
+ deps.clearRuntimeRecovery(message.conversationRef);
33
+ await deps.send({
34
+ type: 'ack_received',
35
+ conversationRef: message.conversationRef,
36
+ messageId: message.messageId,
37
+ emoji: deps.config.feishu.reactEmoji,
38
+ });
39
+ logInfo('receiver', 'ack_sent', {
40
+ messageId: message.messageId,
41
+ conversationId: message.conversationRef.conversationId,
42
+ });
43
+
44
+ const effectiveSessionForStart = deps.sessionStore.ensureSession(message.conversationRef);
45
+ const thread = effectiveSessionForStart.threadId
46
+ ? await deps.runtime.resumeThread(effectiveSessionForStart.threadId, deps.config.workspaceRoot)
47
+ : await deps.runtime.startThread(deps.config.workspaceRoot);
48
+
49
+ logInfo('receiver', 'thread_ready', {
50
+ messageId: message.messageId,
51
+ conversationId: message.conversationRef.conversationId,
52
+ threadId: thread.id,
53
+ resumed: Boolean(effectiveSessionForStart.threadId),
54
+ });
55
+ deps.sessionStore.bindThread(message.conversationRef, thread.id);
56
+ return thread;
57
+ }
@@ -0,0 +1,94 @@
1
+ import { noteStaleTurnSuppressed as recordStaleTurnSuppressed, type StaleTurnSuppressedDetails } from './receiver-recovery.ts';
2
+ import type { SessionStore } from './session-store.ts';
3
+ import type { ConversationRef } from './types.ts';
4
+
5
+ interface TurnCoordinationDeps {
6
+ sessionStore: SessionStore;
7
+ }
8
+
9
+ function currentSessionAdvance(
10
+ sessionStore: SessionStore,
11
+ conversationRef: ConversationRef,
12
+ ): { messageId: string | null; turnId: string | null } {
13
+ const latest = sessionStore.getSession(conversationRef);
14
+ return {
15
+ messageId: latest?.lastMessageId || null,
16
+ turnId: latest?.activeTurnId || null,
17
+ };
18
+ }
19
+
20
+ export function markTurnSuppressed(
21
+ sessionStore: SessionStore,
22
+ conversationRef: ConversationRef,
23
+ turnId: string | null,
24
+ threadId: string | null,
25
+ reason: string,
26
+ ): void {
27
+ if (!turnId) {
28
+ return;
29
+ }
30
+ const latest = currentSessionAdvance(sessionStore, conversationRef);
31
+ sessionStore.markTurnSuppressed(conversationRef, turnId, {
32
+ threadId,
33
+ suppressedByMessageId: latest.messageId,
34
+ suppressedByTurnId: latest.turnId,
35
+ reason,
36
+ });
37
+ }
38
+
39
+ export function isMessageStillLatest(
40
+ sessionStore: SessionStore,
41
+ conversationRef: ConversationRef,
42
+ messageId: string,
43
+ ): boolean {
44
+ const latest = sessionStore.getSession(conversationRef);
45
+ if (!latest) {
46
+ return false;
47
+ }
48
+ return latest.lastMessageId === messageId;
49
+ }
50
+
51
+ export function isTurnStillCurrent(
52
+ sessionStore: SessionStore,
53
+ conversationRef: ConversationRef,
54
+ _messageId: string,
55
+ turnId: string | null,
56
+ ): boolean {
57
+ if (!turnId) {
58
+ return false;
59
+ }
60
+ const latest = sessionStore.getSession(conversationRef);
61
+ if (!latest) {
62
+ return false;
63
+ }
64
+ return latest.activeTurnId === turnId;
65
+ }
66
+
67
+ export function isBlockingTurnStillRelevant(
68
+ sessionStore: SessionStore,
69
+ conversationRef: ConversationRef,
70
+ threadId: string | null,
71
+ turnId: string | null,
72
+ ): boolean {
73
+ if (!threadId || !turnId) {
74
+ return false;
75
+ }
76
+ const latest = sessionStore.getSession(conversationRef);
77
+ if (!latest) {
78
+ return false;
79
+ }
80
+ if (latest.threadId != threadId || latest.activeTurnId != turnId) {
81
+ return false;
82
+ }
83
+ return latest.pendingApprovalIds.length > 0 || Boolean(latest.pendingUserInputRequestId);
84
+ }
85
+
86
+ export function noteStaleTurnSuppressed(
87
+ deps: TurnCoordinationDeps,
88
+ conversationRef: ConversationRef,
89
+ details: StaleTurnSuppressedDetails,
90
+ ): void {
91
+ recordStaleTurnSuppressed({
92
+ markTurnSuppressed: (ref, turnId, threadId, reason) => markTurnSuppressed(deps.sessionStore, ref, turnId, threadId, reason),
93
+ }, conversationRef, details);
94
+ }
@@ -0,0 +1,257 @@
1
+ import { logError } from './logger.ts';
2
+ import { createInboundRuntimeCallbacks } from './receiver-runtime-callbacks.ts';
3
+ import { errorDetailsFromUnknown } from './receiver-helpers.ts';
4
+ import { finalizeTurnResult } from './receiver-turn-result.ts';
5
+ import { isRuntimeInfrastructureError } from './receiver-recovery.ts';
6
+ import { buildPrompt, heartbeatMs, interactiveSandboxPolicy, normalizeErrorReplyText, shouldForwardProgressUpdate } from './receiver-runtime-policy.ts';
7
+ import { handleFailedTurnExecution } from './receiver-turn-failure.ts';
8
+ import { heartbeatMessage, workingNoticeMessage } from './translator.ts';
9
+ import { isBlockingTurnStillRelevant, isTurnStillCurrent, markTurnSuppressed, noteStaleTurnSuppressed } from './receiver-turn-coordination.ts';
10
+ import type { WorkAllyConfig } from './config.ts';
11
+ import type { SessionStore } from './session-store.ts';
12
+ import type { ConversationRef, InboundMessage, RuntimeProgressMeta, RuntimeThreadStatus, RuntimeTurnResult, SessionMeta, UserInputRequest } from './types.ts';
13
+
14
+ interface SendLike {
15
+ (
16
+ outbound: {
17
+ type: 'progress_update' | 'error_reply' | 'status_reply' | 'final_reply';
18
+ conversationRef: ConversationRef;
19
+ text: string;
20
+ },
21
+ extra?: Record<string, unknown>,
22
+ delivery?: { trackTurnId?: string | null },
23
+ ): Promise<boolean>;
24
+ }
25
+
26
+ interface RuntimeLike {
27
+ runTurn(options: {
28
+ threadId: string;
29
+ prompt: string;
30
+ messageId: string;
31
+ cwd: string;
32
+ onTurnStarted?: (turnId: string) => Promise<void> | void;
33
+ onProgress?: (text: string, meta?: RuntimeProgressMeta) => Promise<void> | void;
34
+ onThreadStatus?: (status: RuntimeThreadStatus, meta?: { threadId?: string | null; turnId?: string | null }) => Promise<void> | void;
35
+ onApproval?: (approval: any) => Promise<void> | void;
36
+ onUserInput?: (request: UserInputRequest) => Promise<void> | void;
37
+ approvalPolicy?: 'untrusted' | 'on-failure' | 'on-request' | 'never';
38
+ sandboxPolicy?: unknown;
39
+ }): Promise<RuntimeTurnResult>;
40
+ resolveApproval(requestId: string | number, decision: 'accept' | 'acceptForSession' | 'decline' | 'cancel'): Promise<void>;
41
+ resolveUserInput(requestId: string | number, response: { answers: Record<string, { answers: string[] }> }): Promise<void>;
42
+ resolveElicitation(
43
+ requestId: string | number,
44
+ response: { action: 'accept' | 'decline' | 'cancel'; content: unknown | null },
45
+ ): Promise<void>;
46
+ }
47
+
48
+ interface RuntimeRecoveryRegistry {
49
+ clear(conversationRef: ConversationRef): void;
50
+ isActive(conversationRef: ConversationRef): boolean;
51
+ }
52
+
53
+ interface SupportLike {
54
+ evaluateApprovalAutonomy(approval: any): {
55
+ action: 'accept' | 'decline';
56
+ reason: string;
57
+ policy: string;
58
+ matchedRule?: string;
59
+ ruleSource?: string;
60
+ commandForm?: string;
61
+ } | null;
62
+ scheduleApprovalNotice(
63
+ conversationRef: ConversationRef,
64
+ threadId: string,
65
+ turnId: string,
66
+ noticeState: { sent: boolean },
67
+ ): void;
68
+ tryRecoverTurnResult(
69
+ conversationRef: ConversationRef,
70
+ threadId: string,
71
+ turnId: string | null,
72
+ messageId: string,
73
+ errorText: string,
74
+ startedAt: string | null,
75
+ callbacks: {
76
+ isTurnStillCurrent(conversationRef: ConversationRef, messageId: string, turnId: string | null): boolean;
77
+ markTurnSuppressed(conversationRef: ConversationRef, turnId: string | null, threadId: string | null, reason: string): void;
78
+ },
79
+ ): Promise<RuntimeTurnResult | null>;
80
+ sendRuntimeRecoveryNotice(
81
+ conversationRef: ConversationRef,
82
+ threadId: string,
83
+ turnId: string | null,
84
+ errorText: string,
85
+ recoveryKind: 'infrastructure' | 'final_state_unconfirmed',
86
+ ): Promise<void>;
87
+ recordPendingFinalReplyDelivery(conversationRef: ConversationRef, threadId: string, turnId: string, text: string): string;
88
+ markFinalReplyDelivered(conversationRef: ConversationRef, threadId: string, turnId: string): void;
89
+ }
90
+
91
+ interface ExecuteInboundTurnDeps {
92
+ config: WorkAllyConfig;
93
+ sessionStore: SessionStore;
94
+ runtime: RuntimeLike;
95
+ runtimeRecovery: RuntimeRecoveryRegistry;
96
+ support: SupportLike;
97
+ send: SendLike;
98
+ userRequestedStopTurnIds: Set<string>;
99
+ suppressedInterruptedTurnIds: Set<string>;
100
+ }
101
+
102
+ interface ExecuteInboundTurnArgs {
103
+ message: InboundMessage;
104
+ fallbackSession: SessionMeta;
105
+ threadId: string;
106
+ }
107
+
108
+ export async function executeInboundTurn(
109
+ deps: ExecuteInboundTurnDeps,
110
+ args: ExecuteInboundTurnArgs,
111
+ ): Promise<boolean> {
112
+ const { message, fallbackSession, threadId } = args;
113
+ const startedAt = new Date().toISOString();
114
+ const heartbeatMsValue = heartbeatMs(deps.config);
115
+ let currentTurnId: string | null = null;
116
+ let lastProgressAt = Date.now();
117
+ let finished = false;
118
+ const heartbeat = heartbeatMsValue > 0
119
+ ? setInterval(() => {
120
+ if (finished || deps.runtimeRecovery.isActive(message.conversationRef)) {
121
+ return;
122
+ }
123
+ const latestSession = deps.sessionStore.getSession(message.conversationRef) || fallbackSession;
124
+ if (currentTurnId && !isTurnStillCurrent(deps.sessionStore, message.conversationRef, message.messageId, currentTurnId)) {
125
+ return;
126
+ }
127
+ if (latestSession.pendingApprovalIds.length > 0 || latestSession.pendingUserInputRequestId) {
128
+ return;
129
+ }
130
+ const now = Date.now();
131
+ if (now - lastProgressAt < heartbeatMsValue || !currentTurnId || !deps.runtimeRecovery.isActive(message.conversationRef)) {
132
+ return;
133
+ }
134
+ lastProgressAt = now;
135
+ deps.send({
136
+ type: 'progress_update',
137
+ conversationRef: message.conversationRef,
138
+ text: workingNoticeMessage(heartbeatMessage(latestSession)),
139
+ }, {
140
+ source: 'heartbeat',
141
+ thread_id: latestSession.threadId,
142
+ turn_id: latestSession.activeTurnId,
143
+ }).catch(() => undefined);
144
+ }, Math.min(heartbeatMsValue, 5000))
145
+ : null;
146
+
147
+ let result: RuntimeTurnResult | null = null;
148
+ const waitingApprovalNoticeState = { sent: false };
149
+ let waitingUserInputNoticeSent = false;
150
+ const contextCompactionNoticeKeys = new Set<string>();
151
+ const turnEntryMessageId = message.messageId;
152
+
153
+ try {
154
+ result = await deps.runtime.runTurn({
155
+ threadId,
156
+ prompt: buildPrompt(message),
157
+ messageId: message.messageId,
158
+ cwd: deps.config.workspaceRoot,
159
+ approvalPolicy: deps.config.runtime.approvalPolicy,
160
+ sandboxPolicy: interactiveSandboxPolicy(deps.config),
161
+ ...createInboundRuntimeCallbacks({
162
+ sessionStore: deps.sessionStore,
163
+ runtime: deps.runtime,
164
+ send: deps.send,
165
+ mcpToolApprovalRules: deps.config.runtime.mcpToolApprovalRules,
166
+ clearRuntimeRecovery: (ref) => deps.runtimeRecovery.clear(ref),
167
+ isTurnStillCurrent: (ref, currentMessageId, turnId) => isTurnStillCurrent(deps.sessionStore, ref, currentMessageId, turnId),
168
+ isBlockingTurnStillRelevant: (ref, currentThreadId, turnId) => isBlockingTurnStillRelevant(deps.sessionStore, ref, currentThreadId, turnId),
169
+ noteStaleTurnSuppressed: (ref, details) => noteStaleTurnSuppressed({ sessionStore: deps.sessionStore }, ref, details),
170
+ shouldForwardProgressUpdate: (text, meta) => shouldForwardProgressUpdate(text, meta),
171
+ evaluateApprovalAutonomy: (approval) => deps.support.evaluateApprovalAutonomy(approval),
172
+ scheduleApprovalNotice: (ref, currentThreadId, turnId, noticeState) => deps.support.scheduleApprovalNotice(ref, currentThreadId, turnId, noticeState),
173
+ }, {
174
+ message,
175
+ threadId,
176
+ turnEntryMessageId,
177
+ fallbackSession,
178
+ state: {
179
+ getCurrentTurnId: () => currentTurnId,
180
+ setCurrentTurnId: (turnId) => {
181
+ currentTurnId = turnId;
182
+ },
183
+ setLastProgressAt: (timestamp) => {
184
+ lastProgressAt = timestamp;
185
+ },
186
+ waitingApprovalNoticeState,
187
+ isWaitingUserInputNoticeSent: () => waitingUserInputNoticeSent,
188
+ markWaitingUserInputNoticeSent: () => {
189
+ waitingUserInputNoticeSent = true;
190
+ },
191
+ contextCompactionNoticeKeys,
192
+ },
193
+ }),
194
+ });
195
+ } catch (error) {
196
+ finished = true;
197
+ logError('receiver', 'turn_failed', {
198
+ messageId: message.messageId,
199
+ conversationId: message.conversationRef.conversationId,
200
+ threadId,
201
+ turnId: currentTurnId,
202
+ error: errorDetailsFromUnknown(error),
203
+ });
204
+ if (heartbeat) {
205
+ clearInterval(heartbeat);
206
+ }
207
+ result = await handleFailedTurnExecution({
208
+ sessionStore: deps.sessionStore,
209
+ runtimeRecovery: deps.runtimeRecovery,
210
+ support: deps.support,
211
+ send: deps.send,
212
+ }, {
213
+ message,
214
+ threadId,
215
+ currentTurnId,
216
+ turnEntryMessageId,
217
+ startedAt,
218
+ error,
219
+ });
220
+ if (!result) {
221
+ return true;
222
+ }
223
+ } finally {
224
+ finished = true;
225
+ if (heartbeat) {
226
+ clearInterval(heartbeat);
227
+ }
228
+ }
229
+
230
+ if (!result) {
231
+ return false;
232
+ }
233
+
234
+ return finalizeTurnResult({
235
+ sessionStore: deps.sessionStore,
236
+ send: deps.send,
237
+ clearRuntimeRecovery: (ref) => deps.runtimeRecovery.clear(ref),
238
+ isTurnStillCurrent: (ref, currentMessageId, turnId) => isTurnStillCurrent(deps.sessionStore, ref, currentMessageId, turnId),
239
+ noteStaleTurnSuppressed: (ref, details) => noteStaleTurnSuppressed({ sessionStore: deps.sessionStore }, ref, details),
240
+ markTurnSuppressed: (ref, turnId, currentThreadId, reason) => markTurnSuppressed(deps.sessionStore, ref, turnId, currentThreadId, reason),
241
+ recordPendingFinalReplyDelivery: (ref, currentThreadId, turnId, text) => deps.support.recordPendingFinalReplyDelivery(ref, currentThreadId, turnId, text),
242
+ markFinalReplyDelivered: (ref, currentThreadId, turnId) => deps.support.markFinalReplyDelivered(ref, currentThreadId, turnId),
243
+ isRuntimeInfrastructureError: (text) => isRuntimeInfrastructureError(text),
244
+ normalizeErrorReplyText: (text) => normalizeErrorReplyText(text),
245
+ consumeUserRequestedStop: (turnId) => deps.userRequestedStopTurnIds.delete(turnId),
246
+ hasSuppressedInterruptedTurn: (turnId) => deps.suppressedInterruptedTurnIds.has(turnId),
247
+ clearSuppressedInterruptedTurn: (turnId) => {
248
+ deps.suppressedInterruptedTurnIds.delete(turnId);
249
+ },
250
+ }, {
251
+ conversationRef: message.conversationRef,
252
+ messageId: message.messageId,
253
+ prompt: message.text,
254
+ startedAt,
255
+ result,
256
+ });
257
+ }