feique 1.3.2 → 1.4.0

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 (55) hide show
  1. package/README.en.md +3 -2
  2. package/README.md +3 -2
  3. package/dist/backend/claude.js +28 -54
  4. package/dist/backend/claude.js.map +1 -1
  5. package/dist/backend/factory.d.ts +28 -0
  6. package/dist/backend/factory.js +61 -0
  7. package/dist/backend/factory.js.map +1 -1
  8. package/dist/backend/probe.d.ts +29 -0
  9. package/dist/backend/probe.js +99 -0
  10. package/dist/backend/probe.js.map +1 -0
  11. package/dist/bridge/admin-config.d.ts +47 -0
  12. package/dist/bridge/admin-config.js +141 -0
  13. package/dist/bridge/admin-config.js.map +1 -0
  14. package/dist/bridge/collab-commands.d.ts +42 -0
  15. package/dist/bridge/collab-commands.js +254 -0
  16. package/dist/bridge/collab-commands.js.map +1 -0
  17. package/dist/bridge/commands.d.ts +1 -1
  18. package/dist/bridge/commands.js +3 -0
  19. package/dist/bridge/commands.js.map +1 -1
  20. package/dist/bridge/feishu-commands.d.ts +27 -0
  21. package/dist/bridge/feishu-commands.js +462 -0
  22. package/dist/bridge/feishu-commands.js.map +1 -0
  23. package/dist/bridge/lifecycle.d.ts +46 -0
  24. package/dist/bridge/lifecycle.js +228 -0
  25. package/dist/bridge/lifecycle.js.map +1 -0
  26. package/dist/bridge/memory-commands.d.ts +26 -0
  27. package/dist/bridge/memory-commands.js +330 -0
  28. package/dist/bridge/memory-commands.js.map +1 -0
  29. package/dist/bridge/reply-builders.d.ts +30 -0
  30. package/dist/bridge/reply-builders.js +72 -0
  31. package/dist/bridge/reply-builders.js.map +1 -0
  32. package/dist/bridge/run-pipeline.d.ts +86 -0
  33. package/dist/bridge/run-pipeline.js +442 -0
  34. package/dist/bridge/run-pipeline.js.map +1 -0
  35. package/dist/bridge/run-scheduler.d.ts +47 -0
  36. package/dist/bridge/run-scheduler.js +121 -0
  37. package/dist/bridge/run-scheduler.js.map +1 -0
  38. package/dist/bridge/service-utils.d.ts +47 -0
  39. package/dist/bridge/service-utils.js +309 -0
  40. package/dist/bridge/service-utils.js.map +1 -0
  41. package/dist/bridge/service.d.ts +114 -66
  42. package/dist/bridge/service.js +225 -2196
  43. package/dist/bridge/service.js.map +1 -1
  44. package/dist/config/load.js +1 -1
  45. package/dist/config/load.js.map +1 -1
  46. package/dist/config/paths.js +1 -20
  47. package/dist/config/paths.js.map +1 -1
  48. package/dist/config/schema.d.ts +3 -0
  49. package/dist/config/schema.js +3 -1
  50. package/dist/config/schema.js.map +1 -1
  51. package/dist/feishu/long-connection.js +1 -0
  52. package/dist/feishu/long-connection.js.map +1 -1
  53. package/dist/feishu/webhook.js +1 -0
  54. package/dist/feishu/webhook.js.map +1 -1
  55. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ import type { BridgeConfig } from '../config/schema.js';
2
+ /**
3
+ * Pure reply / card builders pulled out of FeiqueService.
4
+ *
5
+ * Nothing in this module touches instance state — they are all pure
6
+ * functions of their inputs (plus `config` for the two functions that
7
+ * consult service settings). The stateful reply orchestration
8
+ * (runReplyTargets map, sendTextReply, updateRunLifecycleReply) stays
9
+ * on FeiqueService for now; that's a future β step.
10
+ */
11
+ export declare function formatQuotedReply(body: string, _originalText?: string): string;
12
+ export declare function buildReplyTitle(body: string): string;
13
+ export declare function sanitizeUserVisibleReply(body: string): string;
14
+ export declare function stripLifecycleMetadata(body: string): string;
15
+ export declare function supportsInteractiveCardActions(config: BridgeConfig): boolean;
16
+ export declare function resolveRunLifecycleReplyMode(config: BridgeConfig): BridgeConfig['service']['reply_mode'];
17
+ export interface RunLifecycleCardInput {
18
+ title: string;
19
+ body: string;
20
+ projectAlias: string;
21
+ runStatus?: string;
22
+ runPhase?: string;
23
+ cardSummary?: string;
24
+ includeActions?: boolean;
25
+ rerunPayload?: Record<string, unknown>;
26
+ newSessionPayload?: Record<string, unknown>;
27
+ statusPayload?: Record<string, unknown>;
28
+ cancelPayload?: Record<string, unknown>;
29
+ }
30
+ export declare function buildRunLifecycleCard(input: RunLifecycleCardInput): Record<string, unknown>;
@@ -0,0 +1,72 @@
1
+ import { buildMessageCard, buildStatusCard } from '../feishu/cards.js';
2
+ import { truncateForFeishuCard } from '../feishu/text.js';
3
+ import { truncateExcerpt } from './service-utils.js';
4
+ /**
5
+ * Pure reply / card builders pulled out of FeiqueService.
6
+ *
7
+ * Nothing in this module touches instance state — they are all pure
8
+ * functions of their inputs (plus `config` for the two functions that
9
+ * consult service settings). The stateful reply orchestration
10
+ * (runReplyTargets map, sendTextReply, updateRunLifecycleReply) stays
11
+ * on FeiqueService for now; that's a future β step.
12
+ */
13
+ export function formatQuotedReply(body, _originalText) {
14
+ return body;
15
+ }
16
+ export function buildReplyTitle(body) {
17
+ const firstLine = body
18
+ .split(/\r?\n/)
19
+ .map((line) => line.trim())
20
+ .find(Boolean);
21
+ return truncateExcerpt(firstLine ?? '飞鹊 (Feique)', 40);
22
+ }
23
+ export function sanitizeUserVisibleReply(body) {
24
+ return body
25
+ .split(/\r?\n/)
26
+ .filter((line) => !/^(运行|当前运行|阻塞运行|run[_ -]?id|session[_ -]?id|conversation[_ -]?key|chat[_ -]?id|tenant[_ -]?key|project[_ -]?root|pid):/i.test(line.trim()))
27
+ .join('\n')
28
+ .replace(/\n{3,}/g, '\n\n')
29
+ .trim();
30
+ }
31
+ export function stripLifecycleMetadata(body) {
32
+ return body
33
+ .split(/\r?\n/)
34
+ .filter((line) => !/^(项目|处理状态|会话|当前会话|已保存会话数):/.test(line.trim()))
35
+ .join('\n')
36
+ .replace(/\n{3,}/g, '\n\n')
37
+ .trim();
38
+ }
39
+ export function supportsInteractiveCardActions(config) {
40
+ return config.feishu.transport === 'webhook';
41
+ }
42
+ export function resolveRunLifecycleReplyMode(config) {
43
+ if (config.service.reply_mode === 'post') {
44
+ return 'card';
45
+ }
46
+ return config.service.reply_mode;
47
+ }
48
+ export function buildRunLifecycleCard(input) {
49
+ const sanitizedBody = sanitizeUserVisibleReply(input.body);
50
+ if (input.includeActions) {
51
+ return buildStatusCard({
52
+ title: input.title,
53
+ summary: input.cardSummary ?? truncateForFeishuCard(stripLifecycleMetadata(sanitizedBody)),
54
+ projectAlias: input.projectAlias,
55
+ runStatus: input.runStatus,
56
+ runPhase: input.runPhase,
57
+ includeActions: true,
58
+ rerunPayload: input.rerunPayload,
59
+ newSessionPayload: input.newSessionPayload,
60
+ statusPayload: input.statusPayload,
61
+ cancelPayload: input.cancelPayload,
62
+ });
63
+ }
64
+ return buildMessageCard({
65
+ title: input.title,
66
+ body: stripLifecycleMetadata(sanitizedBody),
67
+ status: input.runStatus,
68
+ phase: input.runPhase,
69
+ projectAlias: input.projectAlias,
70
+ });
71
+ }
72
+ //# sourceMappingURL=reply-builders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply-builders.js","sourceRoot":"","sources":["../../src/bridge/reply-builders.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ import type { BridgeConfig, ProjectConfig } from '../config/schema.js';
2
+ import type { IncomingMessageContext } from './types.js';
3
+ import type { Logger } from '../logging.js';
4
+ import type { FeishuClient } from '../feishu/client.js';
5
+ import type { AuditLog } from '../state/audit-log.js';
6
+ import type { SessionStore } from '../state/session-store.js';
7
+ import type { MemoryStore } from '../state/memory-store.js';
8
+ import type { RunStateStore } from '../state/run-state-store.js';
9
+ import type { TrustStore } from '../state/trust-store.js';
10
+ import type { MetricsRegistry } from '../observability/metrics.js';
11
+ import type { Backend, BackendName } from '../backend/types.js';
12
+ import type { CodexSessionIndex } from '../codex/session-index.js';
13
+ import type { FailoverInfo } from '../backend/factory.js';
14
+ import { retrieveMemoryContext } from '../memory/retrieve.js';
15
+ import type { RunState } from '../state/run-state-store.js';
16
+ import type { ActiveRunHandle } from './service.js';
17
+ /**
18
+ * Subset of FeiqueService that the executePrompt pipeline needs.
19
+ *
20
+ * This is the largest host interface in feique because executePrompt is
21
+ * the central run-execution pipeline that touches almost everything in
22
+ * the service: stores, reply rendering, run state, audit, metrics, and
23
+ * collaboration features.
24
+ *
25
+ * Splitting it further into per-phase functions is a future refactor;
26
+ * for now we move the whole 470-line pipeline into a single free
27
+ * function so service.ts can shed the bulk.
28
+ */
29
+ export interface PipelineHost {
30
+ readonly config: BridgeConfig;
31
+ readonly logger: Logger;
32
+ readonly feishuClient: FeishuClient;
33
+ readonly auditLog: AuditLog;
34
+ readonly sessionStore: SessionStore;
35
+ readonly memoryStore: MemoryStore;
36
+ readonly runStateStore: RunStateStore;
37
+ readonly trustStore: TrustStore;
38
+ readonly metrics?: MetricsRegistry;
39
+ readonly codexSessionIndex: CodexSessionIndex;
40
+ readonly activeRuns: Map<string, ActiveRunHandle>;
41
+ readonly runReplyTargets: Map<string, unknown>;
42
+ resolveBackendByName(projectAlias: string, sessionOverride?: BackendName): Backend;
43
+ buildBridgePrompt(projectAlias: string, project: ProjectConfig, incomingMessage: IncomingMessageContext, effectivePrompt: string, memoryContext: Awaited<ReturnType<typeof retrieveMemoryContext>>): Promise<string>;
44
+ appendProjectAuditEvent(projectAlias: string, project: ProjectConfig, event: {
45
+ type: string;
46
+ [key: string]: unknown;
47
+ }): Promise<void>;
48
+ resolveProjectTempDir(projectAlias: string, project: ProjectConfig): string;
49
+ resolveProjectCacheDir(projectAlias: string, project: ProjectConfig): string;
50
+ handleBackendFailover(chatId: string, projectAlias: string, runId: string, info: FailoverInfo): Promise<void>;
51
+ updateRunStartedReply(chatId: string, projectAlias: string, runId: string, backendLabel?: string): Promise<void>;
52
+ updateRunProgressReply(input: {
53
+ chatId: string;
54
+ projectAlias: string;
55
+ prompt: string;
56
+ sessionKey: string;
57
+ replyToMessageId?: string;
58
+ }, runId: string, message: string, backendLabel?: string): Promise<void>;
59
+ sendOrUpdateRunOutcome(input: {
60
+ input: ExecutePromptInput;
61
+ runId: string;
62
+ title: string;
63
+ body: string;
64
+ runStatus: 'success' | 'failure' | 'cancelled';
65
+ runPhase: string;
66
+ cardSummary: string;
67
+ sessionId?: string;
68
+ }): Promise<void>;
69
+ enforceSessionHistoryLimit(conversationKey: string, projectAlias: string): Promise<void>;
70
+ notifyProjectChats(projectAlias: string, text: string): Promise<void>;
71
+ checkAndSendAlerts(completedRun: RunState): Promise<void>;
72
+ }
73
+ export interface ExecutePromptInput {
74
+ runId?: string;
75
+ chatId: string;
76
+ actorId?: string;
77
+ tenantKey?: string;
78
+ projectAlias: string;
79
+ project: ProjectConfig;
80
+ incomingMessage: IncomingMessageContext;
81
+ prompt: string;
82
+ sessionKey: string;
83
+ queueKey: string;
84
+ replyToMessageId?: string;
85
+ }
86
+ export declare function executePrompt(host: PipelineHost, input: ExecutePromptInput): Promise<void>;
@@ -0,0 +1,442 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { resolveProjectBackendWithFailover } from '../backend/factory.js';
4
+ import { retrieveMemoryContext } from '../memory/retrieve.js';
5
+ import { summarizeThreadTurn } from '../memory/summarize.js';
6
+ import { extractInsights } from '../collaboration/knowledge.js';
7
+ import { recordRunOutcome, DEFAULT_TRUST_POLICY } from '../collaboration/trust.js';
8
+ import { buildProjectTimeline, buildOnboardingContext, isNewActor } from '../collaboration/timeline.js';
9
+ import { truncateForFeishuCard } from '../feishu/text.js';
10
+ import { estimateCost } from '../observability/cost.js';
11
+ import { extractFileMarkers, friendlyErrorMessage, truncateExcerpt } from './service-utils.js';
12
+ export async function executePrompt(host, input) {
13
+ const conversation = (await host.sessionStore.getConversation(input.sessionKey)) ??
14
+ (await host.sessionStore.ensureConversation(input.sessionKey, {
15
+ chat_id: input.chatId,
16
+ actor_id: input.actorId,
17
+ tenant_key: input.tenantKey,
18
+ scope: input.project.session_scope,
19
+ }));
20
+ let currentSession = conversation.projects[input.projectAlias];
21
+ // Auto-adopt latest local session when no active session exists
22
+ if (!currentSession?.thread_id && host.config.service.project_switch_auto_adopt_latest) {
23
+ try {
24
+ const sessionBackendOverrideForAdopt = await host.sessionStore.getProjectBackend(input.sessionKey, input.projectAlias);
25
+ const backendForAdopt = host.resolveBackendByName(input.projectAlias, sessionBackendOverrideForAdopt);
26
+ const latestLocal = await backendForAdopt.findLatestSession(input.project.root);
27
+ if (latestLocal) {
28
+ await host.sessionStore.upsertProjectSession(input.sessionKey, input.projectAlias, {
29
+ thread_id: latestLocal.sessionId,
30
+ });
31
+ const refreshed = await host.sessionStore.getConversation(input.sessionKey);
32
+ currentSession = refreshed?.projects[input.projectAlias];
33
+ host.logger.info({ projectAlias: input.projectAlias, sessionId: latestLocal.sessionId, backend: latestLocal.backend }, 'Auto-adopted latest local session for prompt execution');
34
+ }
35
+ }
36
+ catch { /* auto-adopt is best-effort */ }
37
+ }
38
+ if (host.config.service.memory_enabled) {
39
+ await host.memoryStore.cleanupExpiredMemories();
40
+ }
41
+ const memoryContext = host.config.service.memory_enabled
42
+ ? await retrieveMemoryContext(host.memoryStore, {
43
+ conversationKey: input.sessionKey,
44
+ projectAlias: input.projectAlias,
45
+ threadId: currentSession?.thread_id,
46
+ query: input.prompt,
47
+ searchLimit: host.config.service.memory_search_limit,
48
+ groupChatId: input.incomingMessage.chat_type === 'group' ? input.incomingMessage.chat_id : undefined,
49
+ includeGroupMemories: host.config.service.memory_group_enabled && input.incomingMessage.chat_type === 'group',
50
+ })
51
+ : { pinnedMemories: [], relevantMemories: [], pinnedGroupMemories: [], relevantGroupMemories: [] };
52
+ // Direction 6: Inject onboarding context for new actors
53
+ let onboardingPrefix = '';
54
+ if (input.actorId && host.config.service.memory_enabled) {
55
+ try {
56
+ const allRuns = await host.runStateStore.listRuns();
57
+ if (isNewActor(input.actorId, allRuns, input.projectAlias)) {
58
+ const memories = await host.memoryStore.listRecentMemories({ scope: 'project', project_alias: input.projectAlias }, 10);
59
+ const timeline = buildProjectTimeline(allRuns, memories, [], input.projectAlias, 10);
60
+ onboardingPrefix = buildOnboardingContext(timeline, memories, input.projectAlias);
61
+ }
62
+ }
63
+ catch { /* onboarding injection is best-effort */ }
64
+ }
65
+ const effectivePrompt = onboardingPrefix
66
+ ? `${onboardingPrefix}\n\n${input.prompt}`
67
+ : input.prompt;
68
+ const bridgePrompt = await host.buildBridgePrompt(input.projectAlias, input.project, input.incomingMessage, effectivePrompt, memoryContext);
69
+ const startedAt = Date.now();
70
+ const projectRoot = path.resolve(input.project.root);
71
+ const runId = input.runId ?? randomUUID();
72
+ let lastProgressUpdate = 0;
73
+ const activeRun = {
74
+ runId,
75
+ controller: new AbortController(),
76
+ };
77
+ host.activeRuns.set(input.queueKey, activeRun);
78
+ const sessionBackendOverride = await host.sessionStore.getProjectBackend(input.sessionKey, input.projectAlias);
79
+ const failoverResolution = await resolveProjectBackendWithFailover(host.config, input.projectAlias, sessionBackendOverride, host.codexSessionIndex);
80
+ const backend = failoverResolution.backend;
81
+ if (failoverResolution.failover) {
82
+ await host.handleBackendFailover(input.chatId, input.projectAlias, runId, failoverResolution.failover);
83
+ }
84
+ const backendLabel = backend.name === 'claude' ? 'Claude' : 'Codex';
85
+ await host.updateRunStartedReply(input.chatId, input.projectAlias, runId, backendLabel);
86
+ await host.runStateStore.upsertRun(runId, {
87
+ queue_key: input.queueKey,
88
+ conversation_key: input.sessionKey,
89
+ project_alias: input.projectAlias,
90
+ chat_id: input.chatId,
91
+ actor_id: input.actorId,
92
+ actor_name: input.incomingMessage.actor_name,
93
+ session_id: currentSession?.thread_id,
94
+ project_root: projectRoot,
95
+ prompt_excerpt: truncateExcerpt(input.prompt),
96
+ status: 'running',
97
+ status_detail: undefined,
98
+ });
99
+ await host.auditLog.append({
100
+ type: 'codex.run.started',
101
+ run_id: runId,
102
+ chat_id: input.chatId,
103
+ actor_id: input.actorId,
104
+ project_alias: input.projectAlias,
105
+ conversation_key: input.sessionKey,
106
+ session_id: currentSession?.thread_id,
107
+ prompt: input.prompt,
108
+ });
109
+ await host.appendProjectAuditEvent(input.projectAlias, input.project, {
110
+ type: 'codex.run.started',
111
+ run_id: runId,
112
+ chat_id: input.chatId,
113
+ actor_id: input.actorId,
114
+ session_id: currentSession?.thread_id,
115
+ project_root: projectRoot,
116
+ });
117
+ host.logger.info({
118
+ runId,
119
+ queueKey: input.queueKey,
120
+ sessionKey: input.sessionKey,
121
+ projectAlias: input.projectAlias,
122
+ projectRoot,
123
+ sessionId: currentSession?.thread_id,
124
+ }, 'Codex run started');
125
+ host.metrics?.recordCodexTurnStarted(input.projectAlias, runId);
126
+ try {
127
+ const outputTokenLimit = backend.name === 'claude'
128
+ ? (host.config.claude?.output_token_limit ?? host.config.codex.output_token_limit)
129
+ : host.config.codex.output_token_limit;
130
+ const result = await backend.run({
131
+ workdir: input.project.root,
132
+ prompt: bridgePrompt,
133
+ sessionId: currentSession?.thread_id,
134
+ timeoutMs: backend.name === 'claude'
135
+ ? (host.config.claude?.run_timeout_ms ?? host.config.codex.run_timeout_ms)
136
+ : host.config.codex.run_timeout_ms,
137
+ signal: activeRun.controller.signal,
138
+ logger: host.logger,
139
+ projectConfig: backend.name === 'codex'
140
+ ? {
141
+ profile: input.project.profile ?? host.config.codex.default_profile,
142
+ model: input.project.codex_model,
143
+ sandbox: input.project.codex_sandbox ?? input.project.sandbox ?? host.config.codex.default_sandbox,
144
+ tempDir: host.resolveProjectTempDir(input.projectAlias, input.project),
145
+ cacheDir: host.resolveProjectCacheDir(input.projectAlias, input.project),
146
+ }
147
+ : {
148
+ permissionMode: input.project.claude_permission_mode ?? host.config.claude?.default_permission_mode,
149
+ model: input.project.claude_model ?? host.config.claude?.default_model,
150
+ maxBudgetUsd: input.project.claude_max_budget_usd ?? host.config.claude?.max_budget_usd,
151
+ allowedTools: input.project.claude_allowed_tools ?? host.config.claude?.allowed_tools,
152
+ systemPromptAppend: input.project.claude_system_prompt_append ?? host.config.claude?.system_prompt_append,
153
+ },
154
+ onSpawn: async (pid) => {
155
+ activeRun.pid = pid;
156
+ await host.runStateStore.upsertRun(runId, {
157
+ queue_key: input.queueKey,
158
+ conversation_key: input.sessionKey,
159
+ project_alias: input.projectAlias,
160
+ chat_id: input.chatId,
161
+ actor_id: input.actorId,
162
+ session_id: currentSession?.thread_id,
163
+ project_root: projectRoot,
164
+ prompt_excerpt: truncateExcerpt(input.prompt),
165
+ status: 'running',
166
+ status_detail: undefined,
167
+ pid,
168
+ });
169
+ },
170
+ onEvent: async (event) => {
171
+ if (!host.config.service.emit_progress_updates) {
172
+ return;
173
+ }
174
+ const message = backend.summarizeEvent(event);
175
+ if (!message) {
176
+ return;
177
+ }
178
+ const now = Date.now();
179
+ if (now - lastProgressUpdate < host.config.service.progress_update_interval_ms) {
180
+ return;
181
+ }
182
+ lastProgressUpdate = now;
183
+ await host.updateRunProgressReply(input, runId, message, backendLabel);
184
+ },
185
+ });
186
+ const excerpt = result.finalMessage.slice(0, outputTokenLimit);
187
+ if (!excerpt.trim()) {
188
+ host.logger.warn({
189
+ runId,
190
+ queueKey: input.queueKey,
191
+ sessionKey: input.sessionKey,
192
+ projectAlias: input.projectAlias,
193
+ sessionId: result.sessionId,
194
+ durationMs: Date.now() - startedAt,
195
+ }, 'Codex run completed without a displayable final message');
196
+ }
197
+ // Extract and send any [SEND_FILE:path] markers before text reply
198
+ const { cleanText: excerptWithoutFiles, filePaths } = extractFileMarkers(excerpt);
199
+ if (filePaths.length > 0) {
200
+ for (const filePath of filePaths) {
201
+ try {
202
+ await host.feishuClient.sendFile(input.chatId, filePath);
203
+ host.logger.info({ chatId: input.chatId, filePath }, 'Sent file to Feishu');
204
+ }
205
+ catch (err) {
206
+ const msg = err instanceof Error ? err.message : String(err);
207
+ host.logger.warn({ chatId: input.chatId, filePath, error: msg }, 'Failed to send file to Feishu');
208
+ // Notify user about the failure inline
209
+ excerptWithoutFiles === excerpt || await host.feishuClient.sendText(input.chatId, `⚠️ 文件发送失败: ${filePath}\n${msg}`);
210
+ }
211
+ }
212
+ }
213
+ const finalExcerpt = excerptWithoutFiles.trim() || excerpt;
214
+ const cardSummary = truncateForFeishuCard(finalExcerpt || `${backendLabel} 已完成,但没有返回可显示文本。`);
215
+ await host.auditLog.append({
216
+ type: 'codex.run.completed',
217
+ run_id: runId,
218
+ chat_id: input.chatId,
219
+ actor_id: input.actorId,
220
+ project_alias: input.projectAlias,
221
+ conversation_key: input.sessionKey,
222
+ session_id: result.sessionId,
223
+ exit_code: result.exitCode,
224
+ duration_ms: Date.now() - startedAt,
225
+ backend: backend.name,
226
+ });
227
+ await host.appendProjectAuditEvent(input.projectAlias, input.project, {
228
+ type: 'codex.run.completed',
229
+ run_id: runId,
230
+ chat_id: input.chatId,
231
+ actor_id: input.actorId,
232
+ session_id: result.sessionId,
233
+ duration_ms: Date.now() - startedAt,
234
+ backend: backend.name,
235
+ });
236
+ host.logger.info({
237
+ runId,
238
+ queueKey: input.queueKey,
239
+ sessionKey: input.sessionKey,
240
+ projectAlias: input.projectAlias,
241
+ sessionId: result.sessionId,
242
+ exitCode: result.exitCode,
243
+ finalMessageChars: excerpt.length,
244
+ durationMs: Date.now() - startedAt,
245
+ }, 'Codex run completed');
246
+ await host.sessionStore.upsertProjectSession(input.sessionKey, input.projectAlias, {
247
+ thread_id: result.sessionId,
248
+ last_prompt: input.prompt,
249
+ last_response_excerpt: excerpt,
250
+ });
251
+ if (host.config.service.memory_enabled && result.sessionId) {
252
+ const summaryDraft = summarizeThreadTurn({
253
+ previousSummary: memoryContext.threadSummary?.summary,
254
+ prompt: input.prompt,
255
+ responseExcerpt: excerpt,
256
+ maxChars: host.config.service.thread_summary_max_chars,
257
+ });
258
+ const threadSummary = await host.memoryStore.upsertThreadSummary({
259
+ conversation_key: input.sessionKey,
260
+ project_alias: input.projectAlias,
261
+ thread_id: result.sessionId,
262
+ summary: summaryDraft.summary,
263
+ recent_prompt: input.prompt,
264
+ recent_response_excerpt: excerpt,
265
+ files_touched: summaryDraft.filesTouched,
266
+ open_tasks: summaryDraft.openTasks,
267
+ decisions: summaryDraft.decisions,
268
+ });
269
+ await host.auditLog.append({
270
+ type: 'memory.thread_summary.updated',
271
+ run_id: runId,
272
+ project_alias: input.projectAlias,
273
+ conversation_key: input.sessionKey,
274
+ thread_id: result.sessionId,
275
+ files_touched: threadSummary.files_touched,
276
+ });
277
+ }
278
+ await host.enforceSessionHistoryLimit(input.sessionKey, input.projectAlias);
279
+ await host.runStateStore.upsertRun(runId, {
280
+ queue_key: input.queueKey,
281
+ conversation_key: input.sessionKey,
282
+ project_alias: input.projectAlias,
283
+ chat_id: input.chatId,
284
+ actor_id: input.actorId,
285
+ session_id: result.sessionId,
286
+ project_root: projectRoot,
287
+ pid: activeRun.pid,
288
+ prompt_excerpt: truncateExcerpt(input.prompt),
289
+ status: 'success',
290
+ status_detail: undefined,
291
+ input_tokens: result.inputTokens,
292
+ output_tokens: result.outputTokens,
293
+ estimated_cost_usd: estimateCost(result.inputTokens, result.outputTokens, backend.name),
294
+ });
295
+ host.metrics?.recordCodexTurn('success', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
296
+ // Record cost and token metrics
297
+ if (result.inputTokens || result.outputTokens) {
298
+ const costUsd = estimateCost(result.inputTokens, result.outputTokens, backend.name) ?? 0;
299
+ host.metrics?.recordCost(input.projectAlias, backend.name, costUsd);
300
+ host.metrics?.recordTokens(input.projectAlias, backend.name, result.inputTokens ?? 0, result.outputTokens ?? 0);
301
+ }
302
+ // Direction 5: Record trust outcome
303
+ try {
304
+ const trustState = await host.trustStore.getOrCreate(input.projectAlias);
305
+ const updated = recordRunOutcome(trustState, true, DEFAULT_TRUST_POLICY);
306
+ await host.trustStore.update(input.projectAlias, updated);
307
+ host.metrics?.recordTrustLevel(input.projectAlias, updated.current_level);
308
+ }
309
+ catch { /* trust tracking is best-effort */ }
310
+ // Proactive alerts: check if this run triggers any team alerts
311
+ try {
312
+ const completedRunState = await host.runStateStore.getRun(runId);
313
+ if (completedRunState) {
314
+ await host.checkAndSendAlerts(completedRunState);
315
+ }
316
+ }
317
+ catch { /* alerts are best-effort */ }
318
+ // Direction 2: Auto-extract knowledge
319
+ if (host.config.service.memory_enabled && excerpt.length >= 100) {
320
+ try {
321
+ const insight = extractInsights(input.prompt, excerpt, input.projectAlias);
322
+ if (insight) {
323
+ await host.memoryStore.saveProjectMemory({
324
+ project_alias: insight.project_alias,
325
+ title: insight.title,
326
+ content: insight.content,
327
+ tags: insight.tags,
328
+ source: 'auto',
329
+ created_by: input.actorId,
330
+ });
331
+ }
332
+ }
333
+ catch { /* auto-extraction is best-effort */ }
334
+ }
335
+ await host.sendOrUpdateRunOutcome({
336
+ input,
337
+ runId,
338
+ title: `${backendLabel} 已完成`,
339
+ body: finalExcerpt || `${backendLabel} 已完成,但没有返回可显示文本。`,
340
+ runStatus: 'success',
341
+ runPhase: '已完成',
342
+ cardSummary,
343
+ sessionId: result.sessionId,
344
+ });
345
+ }
346
+ catch (error) {
347
+ const message = error instanceof Error ? error.message : String(error);
348
+ const cancelled = error instanceof Error && error.name === 'AbortError' && activeRun.cancelReason === 'user';
349
+ const status = cancelled ? 'cancelled' : 'failure';
350
+ if (!cancelled && error instanceof Error && error.name === 'AbortError') {
351
+ activeRun.cancelReason = 'timeout';
352
+ }
353
+ if (!cancelled && activeRun.cancelReason === 'timeout') {
354
+ host.metrics?.recordCodexTurn('failure', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
355
+ }
356
+ else {
357
+ host.metrics?.recordCodexTurn(cancelled ? 'cancelled' : 'failure', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
358
+ }
359
+ await host.runStateStore.upsertRun(runId, {
360
+ queue_key: input.queueKey,
361
+ conversation_key: input.sessionKey,
362
+ project_alias: input.projectAlias,
363
+ chat_id: input.chatId,
364
+ actor_id: input.actorId,
365
+ session_id: currentSession?.thread_id,
366
+ project_root: projectRoot,
367
+ pid: activeRun.pid,
368
+ prompt_excerpt: truncateExcerpt(input.prompt),
369
+ status,
370
+ status_detail: undefined,
371
+ error: message,
372
+ });
373
+ await host.auditLog.append({
374
+ type: cancelled ? 'codex.run.cancelled' : 'codex.run.failed',
375
+ run_id: runId,
376
+ chat_id: input.chatId,
377
+ actor_id: input.actorId,
378
+ project_alias: input.projectAlias,
379
+ conversation_key: input.sessionKey,
380
+ error: message,
381
+ });
382
+ await host.appendProjectAuditEvent(input.projectAlias, input.project, {
383
+ type: cancelled ? 'codex.run.cancelled' : 'codex.run.failed',
384
+ run_id: runId,
385
+ chat_id: input.chatId,
386
+ actor_id: input.actorId,
387
+ error: message,
388
+ });
389
+ // Direction 5: Record trust failure (only for actual failures, not cancellations)
390
+ if (!cancelled) {
391
+ try {
392
+ const trustState = await host.trustStore.getOrCreate(input.projectAlias);
393
+ const updated = recordRunOutcome(trustState, false, DEFAULT_TRUST_POLICY);
394
+ await host.trustStore.update(input.projectAlias, updated);
395
+ }
396
+ catch { /* trust tracking is best-effort */ }
397
+ // Notify project chats about the failure
398
+ await host.notifyProjectChats(input.projectAlias, `❌ 运行失败 [${input.projectAlias}]\n${message.slice(0, 200)}`);
399
+ // Proactive alerts on failure
400
+ try {
401
+ const failedRunState = await host.runStateStore.getRun(runId);
402
+ if (failedRunState) {
403
+ await host.checkAndSendAlerts(failedRunState);
404
+ }
405
+ }
406
+ catch { /* alerts are best-effort */ }
407
+ }
408
+ if (cancelled) {
409
+ host.logger.warn({
410
+ runId,
411
+ queueKey: input.queueKey,
412
+ sessionKey: input.sessionKey,
413
+ projectAlias: input.projectAlias,
414
+ durationMs: Date.now() - startedAt,
415
+ }, 'Codex run cancelled');
416
+ }
417
+ else {
418
+ host.logger.error({
419
+ error,
420
+ runId,
421
+ queueKey: input.queueKey,
422
+ sessionKey: input.sessionKey,
423
+ projectAlias: input.projectAlias,
424
+ durationMs: Date.now() - startedAt,
425
+ }, 'Codex run failed');
426
+ }
427
+ await host.sendOrUpdateRunOutcome({
428
+ input,
429
+ runId,
430
+ title: cancelled ? '运行已取消' : '执行失败',
431
+ body: cancelled ? '当前运行已取消。' : ['执行失败。', '', friendlyErrorMessage(message)].join('\n'),
432
+ runStatus: cancelled ? 'cancelled' : 'failure',
433
+ runPhase: cancelled ? '已取消' : '失败',
434
+ cardSummary: truncateForFeishuCard(cancelled ? '当前运行已取消。' : friendlyErrorMessage(message)),
435
+ });
436
+ }
437
+ finally {
438
+ host.activeRuns.delete(input.queueKey);
439
+ host.runReplyTargets.delete(runId);
440
+ }
441
+ }
442
+ //# sourceMappingURL=run-pipeline.js.map