@renseiai/agentfactory-nextjs 0.8.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.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/src/__tests__/middleware-edge-safety.test.d.ts +2 -0
- package/dist/src/__tests__/middleware-edge-safety.test.d.ts.map +1 -0
- package/dist/src/__tests__/middleware-edge-safety.test.js +74 -0
- package/dist/src/__tests__/poll-project-filter.test.d.ts +2 -0
- package/dist/src/__tests__/poll-project-filter.test.d.ts.map +1 -0
- package/dist/src/__tests__/poll-project-filter.test.js +83 -0
- package/dist/src/__tests__/subpath-exports.test.d.ts +2 -0
- package/dist/src/__tests__/subpath-exports.test.d.ts.map +1 -0
- package/dist/src/__tests__/subpath-exports.test.js +35 -0
- package/dist/src/__tests__/webhook-project-filter.test.d.ts +2 -0
- package/dist/src/__tests__/webhook-project-filter.test.d.ts.map +1 -0
- package/dist/src/__tests__/webhook-project-filter.test.js +48 -0
- package/dist/src/factory.d.ts +140 -0
- package/dist/src/factory.d.ts.map +1 -0
- package/dist/src/factory.js +127 -0
- package/dist/src/handlers/cleanup.d.ts +44 -0
- package/dist/src/handlers/cleanup.d.ts.map +1 -0
- package/dist/src/handlers/cleanup.js +34 -0
- package/dist/src/handlers/config.d.ts +11 -0
- package/dist/src/handlers/config.d.ts.map +1 -0
- package/dist/src/handlers/config.js +20 -0
- package/dist/src/handlers/issue-tracker-proxy/index.d.ts +34 -0
- package/dist/src/handlers/issue-tracker-proxy/index.d.ts.map +1 -0
- package/dist/src/handlers/issue-tracker-proxy/index.js +230 -0
- package/dist/src/handlers/issue-tracker-proxy/serializer.d.ts +28 -0
- package/dist/src/handlers/issue-tracker-proxy/serializer.d.ts.map +1 -0
- package/dist/src/handlers/issue-tracker-proxy/serializer.js +95 -0
- package/dist/src/handlers/issue-tracker-proxy/types.d.ts +9 -0
- package/dist/src/handlers/issue-tracker-proxy/types.d.ts.map +1 -0
- package/dist/src/handlers/issue-tracker-proxy/types.js +4 -0
- package/dist/src/handlers/oauth/callback.d.ts +36 -0
- package/dist/src/handlers/oauth/callback.d.ts.map +1 -0
- package/dist/src/handlers/oauth/callback.js +96 -0
- package/dist/src/handlers/public/session-detail.d.ts +31 -0
- package/dist/src/handlers/public/session-detail.d.ts.map +1 -0
- package/dist/src/handlers/public/session-detail.js +91 -0
- package/dist/src/handlers/public/sessions-list.d.ts +22 -0
- package/dist/src/handlers/public/sessions-list.d.ts.map +1 -0
- package/dist/src/handlers/public/sessions-list.js +75 -0
- package/dist/src/handlers/public/stats.d.ts +28 -0
- package/dist/src/handlers/public/stats.d.ts.map +1 -0
- package/dist/src/handlers/public/stats.js +66 -0
- package/dist/src/handlers/sessions/activity.d.ts +15 -0
- package/dist/src/handlers/sessions/activity.d.ts.map +1 -0
- package/dist/src/handlers/sessions/activity.js +93 -0
- package/dist/src/handlers/sessions/claim.d.ts +15 -0
- package/dist/src/handlers/sessions/claim.d.ts.map +1 -0
- package/dist/src/handlers/sessions/claim.js +139 -0
- package/dist/src/handlers/sessions/completion.d.ts +16 -0
- package/dist/src/handlers/sessions/completion.d.ts.map +1 -0
- package/dist/src/handlers/sessions/completion.js +82 -0
- package/dist/src/handlers/sessions/external-urls.d.ts +15 -0
- package/dist/src/handlers/sessions/external-urls.d.ts.map +1 -0
- package/dist/src/handlers/sessions/external-urls.js +70 -0
- package/dist/src/handlers/sessions/get.d.ts +19 -0
- package/dist/src/handlers/sessions/get.d.ts.map +1 -0
- package/dist/src/handlers/sessions/get.js +47 -0
- package/dist/src/handlers/sessions/list.d.ts +27 -0
- package/dist/src/handlers/sessions/list.d.ts.map +1 -0
- package/dist/src/handlers/sessions/list.js +51 -0
- package/dist/src/handlers/sessions/lock-refresh.d.ts +14 -0
- package/dist/src/handlers/sessions/lock-refresh.d.ts.map +1 -0
- package/dist/src/handlers/sessions/lock-refresh.js +38 -0
- package/dist/src/handlers/sessions/progress.d.ts +15 -0
- package/dist/src/handlers/sessions/progress.d.ts.map +1 -0
- package/dist/src/handlers/sessions/progress.js +94 -0
- package/dist/src/handlers/sessions/prompts.d.ts +15 -0
- package/dist/src/handlers/sessions/prompts.d.ts.map +1 -0
- package/dist/src/handlers/sessions/prompts.js +91 -0
- package/dist/src/handlers/sessions/status.d.ts +19 -0
- package/dist/src/handlers/sessions/status.d.ts.map +1 -0
- package/dist/src/handlers/sessions/status.js +187 -0
- package/dist/src/handlers/sessions/tool-error.d.ts +15 -0
- package/dist/src/handlers/sessions/tool-error.d.ts.map +1 -0
- package/dist/src/handlers/sessions/tool-error.js +103 -0
- package/dist/src/handlers/sessions/transfer-ownership.d.ts +14 -0
- package/dist/src/handlers/sessions/transfer-ownership.d.ts.map +1 -0
- package/dist/src/handlers/sessions/transfer-ownership.js +56 -0
- package/dist/src/handlers/workers/get-delete.d.ts +15 -0
- package/dist/src/handlers/workers/get-delete.d.ts.map +1 -0
- package/dist/src/handlers/workers/get-delete.js +58 -0
- package/dist/src/handlers/workers/heartbeat.d.ts +14 -0
- package/dist/src/handlers/workers/heartbeat.d.ts.map +1 -0
- package/dist/src/handlers/workers/heartbeat.js +42 -0
- package/dist/src/handlers/workers/list.d.ts +22 -0
- package/dist/src/handlers/workers/list.d.ts.map +1 -0
- package/dist/src/handlers/workers/list.js +33 -0
- package/dist/src/handlers/workers/poll.d.ts +14 -0
- package/dist/src/handlers/workers/poll.d.ts.map +1 -0
- package/dist/src/handlers/workers/poll.js +96 -0
- package/dist/src/handlers/workers/register.d.ts +9 -0
- package/dist/src/handlers/workers/register.d.ts.map +1 -0
- package/dist/src/handlers/workers/register.js +45 -0
- package/dist/src/index.d.ts +52 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +56 -0
- package/dist/src/linear-client-resolver.d.ts +59 -0
- package/dist/src/linear-client-resolver.d.ts.map +1 -0
- package/dist/src/linear-client-resolver.js +104 -0
- package/dist/src/middleware/cron-auth.d.ts +21 -0
- package/dist/src/middleware/cron-auth.d.ts.map +1 -0
- package/dist/src/middleware/cron-auth.js +46 -0
- package/dist/src/middleware/factory.d.ts +33 -0
- package/dist/src/middleware/factory.d.ts.map +1 -0
- package/dist/src/middleware/factory.js +185 -0
- package/dist/src/middleware/index.d.ts +16 -0
- package/dist/src/middleware/index.d.ts.map +1 -0
- package/dist/src/middleware/index.js +14 -0
- package/dist/src/middleware/types.d.ts +35 -0
- package/dist/src/middleware/types.d.ts.map +1 -0
- package/dist/src/middleware/types.js +4 -0
- package/dist/src/middleware/worker-auth.d.ts +25 -0
- package/dist/src/middleware/worker-auth.d.ts.map +1 -0
- package/dist/src/middleware/worker-auth.js +43 -0
- package/dist/src/orchestrator/error-formatting.d.ts +8 -0
- package/dist/src/orchestrator/error-formatting.d.ts.map +1 -0
- package/dist/src/orchestrator/error-formatting.js +35 -0
- package/dist/src/orchestrator/index.d.ts +4 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -0
- package/dist/src/orchestrator/index.js +2 -0
- package/dist/src/orchestrator/types.d.ts +53 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +4 -0
- package/dist/src/orchestrator/webhook-orchestrator.d.ts +32 -0
- package/dist/src/orchestrator/webhook-orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator/webhook-orchestrator.js +373 -0
- package/dist/src/types.d.ts +101 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +7 -0
- package/dist/src/webhook/governor-bridge.d.ts +23 -0
- package/dist/src/webhook/governor-bridge.d.ts.map +1 -0
- package/dist/src/webhook/governor-bridge.js +36 -0
- package/dist/src/webhook/handlers/issue-updated.d.ts +15 -0
- package/dist/src/webhook/handlers/issue-updated.d.ts.map +1 -0
- package/dist/src/webhook/handlers/issue-updated.js +771 -0
- package/dist/src/webhook/handlers/session-created.d.ts +9 -0
- package/dist/src/webhook/handlers/session-created.d.ts.map +1 -0
- package/dist/src/webhook/handlers/session-created.js +337 -0
- package/dist/src/webhook/handlers/session-prompted.d.ts +9 -0
- package/dist/src/webhook/handlers/session-prompted.d.ts.map +1 -0
- package/dist/src/webhook/handlers/session-prompted.js +199 -0
- package/dist/src/webhook/handlers/session-updated.d.ts +9 -0
- package/dist/src/webhook/handlers/session-updated.d.ts.map +1 -0
- package/dist/src/webhook/handlers/session-updated.js +29 -0
- package/dist/src/webhook/processor.d.ts +22 -0
- package/dist/src/webhook/processor.d.ts.map +1 -0
- package/dist/src/webhook/processor.js +98 -0
- package/dist/src/webhook/signature.d.ts +16 -0
- package/dist/src/webhook/signature.d.ts.map +1 -0
- package/dist/src/webhook/signature.js +23 -0
- package/dist/src/webhook/utils.d.ts +61 -0
- package/dist/src/webhook/utils.d.ts.map +1 -0
- package/dist/src/webhook/utils.js +166 -0
- package/package.json +86 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle agent session 'create' events — new session initiated.
|
|
3
|
+
*/
|
|
4
|
+
import { NextResponse } from 'next/server';
|
|
5
|
+
import type { LinearWebhookPayload } from '@renseiai/agentfactory-linear';
|
|
6
|
+
import type { ResolvedWebhookConfig } from '../../types.js';
|
|
7
|
+
import type { createLogger } from '@renseiai/agentfactory-server';
|
|
8
|
+
export declare function handleSessionCreated(config: ResolvedWebhookConfig, payload: LinearWebhookPayload, rawPayload: Record<string, unknown>, log: ReturnType<typeof createLogger>): Promise<NextResponse | null>;
|
|
9
|
+
//# sourceMappingURL=session-created.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-created.d.ts","sourceRoot":"","sources":["../../../../src/webhook/handlers/session-created.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAiB,MAAM,+BAA+B,CAAA;AAoBxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAS3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAEjE,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,oBAAoB,EAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAuY9B"}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle agent session 'create' events — new session initiated.
|
|
3
|
+
*/
|
|
4
|
+
import { NextResponse } from 'next/server';
|
|
5
|
+
import { TERMINAL_STATUSES, validateWorkTypeForStatus, WORK_TYPE_ALLOWED_STATUSES, STATUS_WORK_TYPE_MAP, getValidWorkTypesForStatus, buildFailureContextBlock, } from '@renseiai/agentfactory-linear';
|
|
6
|
+
import { generateIdempotencyKey, isWebhookProcessed, storeSessionState, getSessionState, updateSessionStatus, dispatchWork, getWorkflowState, } from '@renseiai/agentfactory-server';
|
|
7
|
+
import { emitActivity, determineWorkType, isProjectAllowed, getAppUrl, getPriority, WORK_TYPE_MESSAGES, } from '../utils.js';
|
|
8
|
+
export async function handleSessionCreated(config, payload, rawPayload, log) {
|
|
9
|
+
const agentSession = rawPayload.agentSession;
|
|
10
|
+
log.debug('AgentSessionEvent payload structure', {
|
|
11
|
+
payloadKeys: Object.keys(rawPayload),
|
|
12
|
+
agentSessionKeys: agentSession ? Object.keys(agentSession) : [],
|
|
13
|
+
hasAgentSession: !!agentSession,
|
|
14
|
+
});
|
|
15
|
+
if (!agentSession) {
|
|
16
|
+
log.error('AgentSessionEvent missing agentSession field', {
|
|
17
|
+
payloadKeys: Object.keys(rawPayload),
|
|
18
|
+
});
|
|
19
|
+
return NextResponse.json({ error: 'Missing agentSession in webhook payload', success: false }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
const sessionId = agentSession.id;
|
|
22
|
+
const issue = agentSession.issue;
|
|
23
|
+
const issueId = agentSession.issueId || issue?.id;
|
|
24
|
+
const webhookId = rawPayload.webhookId;
|
|
25
|
+
const agentId = agentSession.agentId;
|
|
26
|
+
if (!sessionId || !issueId) {
|
|
27
|
+
log.error('Missing sessionId or issueId in webhook payload', {
|
|
28
|
+
sessionId,
|
|
29
|
+
issueId,
|
|
30
|
+
agentSessionKeys: Object.keys(agentSession),
|
|
31
|
+
});
|
|
32
|
+
return NextResponse.json({ error: 'Invalid payload structure', success: false }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
const sessionLog = log.child({ sessionId, issueId });
|
|
35
|
+
const promptContext = rawPayload.promptContext;
|
|
36
|
+
const user = rawPayload.user;
|
|
37
|
+
const comment = rawPayload.comment;
|
|
38
|
+
const isMention = !!comment;
|
|
39
|
+
const initiationType = isMention ? 'mention' : 'delegation';
|
|
40
|
+
sessionLog.info('Agent session created', {
|
|
41
|
+
initiationType,
|
|
42
|
+
isMention,
|
|
43
|
+
hasPromptContext: !!promptContext,
|
|
44
|
+
promptContextLength: promptContext?.length,
|
|
45
|
+
userName: user?.name,
|
|
46
|
+
});
|
|
47
|
+
// Idempotency check
|
|
48
|
+
const idempotencyKey = generateIdempotencyKey(webhookId, sessionId);
|
|
49
|
+
if (await isWebhookProcessed(idempotencyKey)) {
|
|
50
|
+
sessionLog.info('Duplicate webhook ignored', { idempotencyKey });
|
|
51
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'duplicate_webhook' });
|
|
52
|
+
}
|
|
53
|
+
const existingSession = await getSessionState(sessionId);
|
|
54
|
+
if (existingSession) {
|
|
55
|
+
sessionLog.info('Session already exists', { status: existingSession.status });
|
|
56
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'session_already_exists' });
|
|
57
|
+
}
|
|
58
|
+
const issueIdentifier = issue?.identifier || issueId.slice(0, 8);
|
|
59
|
+
// Determine work type
|
|
60
|
+
let workType = 'development';
|
|
61
|
+
let currentStatus;
|
|
62
|
+
let projectName;
|
|
63
|
+
let workTypeSource = 'status';
|
|
64
|
+
// Fetch current issue status
|
|
65
|
+
try {
|
|
66
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
67
|
+
const issueDetails = await linearClient.getIssue(issueId);
|
|
68
|
+
const currentState = await issueDetails.state;
|
|
69
|
+
currentStatus = currentState?.name;
|
|
70
|
+
sessionLog.debug('Fetched current issue status', { currentStatus });
|
|
71
|
+
// Extract project name for routing
|
|
72
|
+
const project = await issueDetails.project;
|
|
73
|
+
projectName = project?.name;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
sessionLog.warn('Failed to fetch issue status', { error: err });
|
|
77
|
+
}
|
|
78
|
+
// Server-level project filter
|
|
79
|
+
if (!isProjectAllowed(projectName, config.projects ?? [])) {
|
|
80
|
+
sessionLog.debug('Project not handled by this server, skipping', { projectName });
|
|
81
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'project_not_allowed' });
|
|
82
|
+
}
|
|
83
|
+
// Phase 1: Mention-based routing
|
|
84
|
+
if (isMention && promptContext && config.detectWorkTypeFromPrompt) {
|
|
85
|
+
// For mentions: unconstrained detection (pass all work types)
|
|
86
|
+
const allWorkTypes = [
|
|
87
|
+
'coordination', 'backlog-creation', 'research', 'qa', 'inflight',
|
|
88
|
+
'acceptance', 'refinement', 'development', 'qa-coordination', 'acceptance-coordination',
|
|
89
|
+
];
|
|
90
|
+
const mentionWorkType = config.detectWorkTypeFromPrompt(promptContext, allWorkTypes);
|
|
91
|
+
if (mentionWorkType) {
|
|
92
|
+
workType = mentionWorkType;
|
|
93
|
+
workTypeSource = 'mention';
|
|
94
|
+
sessionLog.info('Detected work type from mention prompt', {
|
|
95
|
+
workType,
|
|
96
|
+
currentStatus,
|
|
97
|
+
promptPreview: promptContext.substring(0, 50),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// For non-mentions or when mention detection failed: constrained detection
|
|
102
|
+
if (workTypeSource === 'status' && promptContext && currentStatus && config.detectWorkTypeFromPrompt) {
|
|
103
|
+
const validWorkTypes = getValidWorkTypesForStatus(currentStatus);
|
|
104
|
+
const constrainedWorkType = config.detectWorkTypeFromPrompt(promptContext, validWorkTypes);
|
|
105
|
+
if (constrainedWorkType) {
|
|
106
|
+
workType = constrainedWorkType;
|
|
107
|
+
workTypeSource = 'mention';
|
|
108
|
+
sessionLog.info('Detected work type from promptContext (constrained)', {
|
|
109
|
+
workType,
|
|
110
|
+
currentStatus,
|
|
111
|
+
validWorkTypes,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Phase 2: Fall back to status-based routing
|
|
116
|
+
if (workTypeSource === 'status') {
|
|
117
|
+
workType = determineWorkType(currentStatus);
|
|
118
|
+
sessionLog.info('Detected work type from issue status', {
|
|
119
|
+
currentStatus,
|
|
120
|
+
workType,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Phase 2.5: Auto-detect parent/child issues for coordination routing
|
|
124
|
+
// Apply to all status-derived work types that have coordination variants
|
|
125
|
+
const coordinationUpgradeable = workTypeSource === 'status' && (workType === 'development' || workType === 'refinement');
|
|
126
|
+
if (coordinationUpgradeable) {
|
|
127
|
+
try {
|
|
128
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
129
|
+
// Skip child/sub-issues for non-mention sessions — these are managed
|
|
130
|
+
// by the parent issue's coordinator agent, not dispatched independently.
|
|
131
|
+
if (workType === 'development') {
|
|
132
|
+
const isChild = await linearClient.isChildIssue(issueId);
|
|
133
|
+
if (isChild) {
|
|
134
|
+
sessionLog.info('Sub-issue detected, skipping independent agent dispatch', {
|
|
135
|
+
issueIdentifier,
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
await emitActivity(linearClient, sessionId, 'response', `This is a sub-issue. Work will be coordinated by the parent issue's agent.`);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
sessionLog.warn('Failed to emit sub-issue skip activity', { error: err });
|
|
142
|
+
}
|
|
143
|
+
return NextResponse.json({
|
|
144
|
+
success: true,
|
|
145
|
+
skipped: true,
|
|
146
|
+
reason: 'sub_issue_managed_by_parent',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const isParent = await linearClient.isParentIssue(issueId);
|
|
151
|
+
if (isParent) {
|
|
152
|
+
if (workType === 'development')
|
|
153
|
+
workType = 'coordination';
|
|
154
|
+
else if (workType === 'refinement')
|
|
155
|
+
workType = 'refinement-coordination';
|
|
156
|
+
sessionLog.info('Parent issue detected, switching to coordination work type', {
|
|
157
|
+
issueIdentifier,
|
|
158
|
+
workType,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
sessionLog.warn('Failed to check issue parent/child status', { error: err });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Check terminal state
|
|
167
|
+
if (currentStatus && TERMINAL_STATUSES.includes(currentStatus)) {
|
|
168
|
+
sessionLog.info('Issue in terminal state, acknowledging mention', { currentStatus });
|
|
169
|
+
try {
|
|
170
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
171
|
+
await emitActivity(linearClient, sessionId, 'response', `This issue is in **${currentStatus}** status and has been completed. No further agent work is needed.\n\n` +
|
|
172
|
+
`If you need additional help, please create a new issue or reopen this one by moving it back to an active status.`);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
sessionLog.error('Failed to emit terminal state response', { error: err });
|
|
176
|
+
}
|
|
177
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'terminal_state', currentStatus });
|
|
178
|
+
}
|
|
179
|
+
// Validate work type for status
|
|
180
|
+
if (currentStatus) {
|
|
181
|
+
const validation = validateWorkTypeForStatus(workType, currentStatus);
|
|
182
|
+
if (!validation.valid) {
|
|
183
|
+
sessionLog.warn('Work type validation failed', { workType, currentStatus, error: validation.error });
|
|
184
|
+
try {
|
|
185
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
186
|
+
const allowedStatuses = WORK_TYPE_ALLOWED_STATUSES[workType];
|
|
187
|
+
const expectedWorkType = STATUS_WORK_TYPE_MAP[currentStatus];
|
|
188
|
+
await emitActivity(linearClient, sessionId, 'error', `Cannot perform ${workType} work on this issue.\n\n` +
|
|
189
|
+
`**Current status:** ${currentStatus}\n` +
|
|
190
|
+
`**${workType} requires status:** ${allowedStatuses.join(' or ')}\n\n` +
|
|
191
|
+
(expectedWorkType
|
|
192
|
+
? `For issues in ${currentStatus} status, use ${expectedWorkType} commands instead.`
|
|
193
|
+
: `This issue's status (${currentStatus}) is not handled by the agent.`));
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
sessionLog.error('Failed to emit validation error activity', { error: err });
|
|
197
|
+
}
|
|
198
|
+
return NextResponse.json({
|
|
199
|
+
success: false,
|
|
200
|
+
error: 'work_type_invalid_for_status',
|
|
201
|
+
message: validation.error,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Extract PR info for QA/acceptance agents so they know which PRs to validate
|
|
206
|
+
let prContext = '';
|
|
207
|
+
const needsPrContext = workType === 'qa' ||
|
|
208
|
+
workType === 'acceptance' ||
|
|
209
|
+
workType === 'qa-coordination' ||
|
|
210
|
+
workType === 'acceptance-coordination';
|
|
211
|
+
if (needsPrContext) {
|
|
212
|
+
try {
|
|
213
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
214
|
+
const issueForAttachments = await linearClient.getIssue(issueId);
|
|
215
|
+
const attachments = await issueForAttachments.attachments();
|
|
216
|
+
const prLinks = attachments.nodes
|
|
217
|
+
.filter((a) => a.url?.includes('github.com') && a.url?.includes('/pull/'))
|
|
218
|
+
.map((a) => {
|
|
219
|
+
const match = a.url.match(/\/pull\/(\d+)/);
|
|
220
|
+
return match ? { url: a.url, number: parseInt(match[1], 10), title: a.title ?? '' } : null;
|
|
221
|
+
})
|
|
222
|
+
.filter((pr) => pr !== null);
|
|
223
|
+
if (prLinks.length > 0) {
|
|
224
|
+
prContext = '\n\nLinked PRs:\n';
|
|
225
|
+
prContext += prLinks.map((pr) => `- PR #${pr.number}: ${pr.title} (${pr.url})`).join('\n');
|
|
226
|
+
if (prLinks.length > 1) {
|
|
227
|
+
prContext +=
|
|
228
|
+
'\n\nNote: Multiple PRs are linked. Check each PR state (open/merged/closed) and validate the most recent OPEN one.';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
sessionLog.debug('Extracted PR context for QA/acceptance', {
|
|
232
|
+
prLinksCount: prLinks.length,
|
|
233
|
+
hasPrContext: prContext.length > 0,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
sessionLog.warn('Failed to extract PR info from issue attachments', { error: err });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const enhancedPromptContext = prContext
|
|
241
|
+
? promptContext
|
|
242
|
+
? promptContext + prContext
|
|
243
|
+
: prContext
|
|
244
|
+
: promptContext;
|
|
245
|
+
const priority = getPriority(config, workType);
|
|
246
|
+
await storeSessionState(sessionId, {
|
|
247
|
+
issueId,
|
|
248
|
+
issueIdentifier,
|
|
249
|
+
providerSessionId: null,
|
|
250
|
+
worktreePath: '',
|
|
251
|
+
status: 'pending',
|
|
252
|
+
queuedAt: Date.now(),
|
|
253
|
+
promptContext: promptContext,
|
|
254
|
+
priority,
|
|
255
|
+
organizationId: payload.organizationId,
|
|
256
|
+
workType,
|
|
257
|
+
agentId,
|
|
258
|
+
projectName,
|
|
259
|
+
});
|
|
260
|
+
// Enrich prompt with workflow failure context for retries
|
|
261
|
+
// Applies to: refinement, development (rework from Backlog), coordination (rework from Started)
|
|
262
|
+
let workflowContextBlock = '';
|
|
263
|
+
let wfContext;
|
|
264
|
+
const needsFailureContext = workType === 'refinement' || workType === 'refinement-coordination' ||
|
|
265
|
+
(workType === 'development' && currentStatus === 'Backlog') ||
|
|
266
|
+
(workType === 'coordination' && currentStatus === 'Started');
|
|
267
|
+
if (needsFailureContext) {
|
|
268
|
+
try {
|
|
269
|
+
const workflowState = await getWorkflowState(issueId);
|
|
270
|
+
if (workflowState && workflowState.cycleCount > 0) {
|
|
271
|
+
wfContext = {
|
|
272
|
+
cycleCount: workflowState.cycleCount,
|
|
273
|
+
strategy: workflowState.strategy,
|
|
274
|
+
failureSummary: workflowState.failureSummary,
|
|
275
|
+
};
|
|
276
|
+
workflowContextBlock = buildFailureContextBlock(workType, wfContext);
|
|
277
|
+
if (workflowContextBlock) {
|
|
278
|
+
sessionLog.info('Prompt enriched with workflow failure context', {
|
|
279
|
+
workType,
|
|
280
|
+
cycleCount: workflowState.cycleCount,
|
|
281
|
+
strategy: workflowState.strategy,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
sessionLog.warn('Failed to enrich prompt with workflow context', { error: err });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Queue work
|
|
291
|
+
const work = {
|
|
292
|
+
sessionId,
|
|
293
|
+
issueId,
|
|
294
|
+
issueIdentifier,
|
|
295
|
+
priority,
|
|
296
|
+
queuedAt: Date.now(),
|
|
297
|
+
workType,
|
|
298
|
+
prompt: config.generatePrompt(issueIdentifier, workType, enhancedPromptContext, wfContext) + workflowContextBlock,
|
|
299
|
+
projectName,
|
|
300
|
+
};
|
|
301
|
+
const result = await dispatchWork(work);
|
|
302
|
+
if (result.dispatched || result.parked) {
|
|
303
|
+
sessionLog.info('Work dispatched', {
|
|
304
|
+
sessionId,
|
|
305
|
+
issueIdentifier,
|
|
306
|
+
workType,
|
|
307
|
+
dispatched: result.dispatched,
|
|
308
|
+
parked: result.parked,
|
|
309
|
+
replaced: result.replaced,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
sessionLog.error('Failed to dispatch work');
|
|
314
|
+
await updateSessionStatus(sessionId, 'failed');
|
|
315
|
+
}
|
|
316
|
+
// Update session with externalUrl and acknowledge
|
|
317
|
+
try {
|
|
318
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
319
|
+
const appUrl = getAppUrl(config);
|
|
320
|
+
await linearClient.updateAgentSession({
|
|
321
|
+
sessionId,
|
|
322
|
+
externalUrls: [
|
|
323
|
+
{
|
|
324
|
+
label: 'Agent Dashboard',
|
|
325
|
+
url: `${appUrl}/sessions/${sessionId}`,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
const activityText = WORK_TYPE_MESSAGES[workType];
|
|
330
|
+
await emitActivity(linearClient, sessionId, 'thought', activityText);
|
|
331
|
+
sessionLog.debug('Session updated and activity emitted');
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
sessionLog.error('Failed to update session or create comment', { error: err });
|
|
335
|
+
}
|
|
336
|
+
return null; // Continue processing
|
|
337
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle agent session 'prompted' events — follow-up messages or stop/continue signals.
|
|
3
|
+
*/
|
|
4
|
+
import { NextResponse } from 'next/server';
|
|
5
|
+
import type { LinearWebhookPayload } from '@renseiai/agentfactory-linear';
|
|
6
|
+
import type { ResolvedWebhookConfig } from '../../types.js';
|
|
7
|
+
import type { createLogger } from '@renseiai/agentfactory-server';
|
|
8
|
+
export declare function handleSessionPrompted(config: ResolvedWebhookConfig, payload: LinearWebhookPayload, rawPayload: Record<string, unknown>, log: ReturnType<typeof createLogger>): Promise<NextResponse | null>;
|
|
9
|
+
//# sourceMappingURL=session-prompted.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-prompted.d.ts","sourceRoot":"","sources":["../../../../src/webhook/handlers/session-prompted.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAiB,MAAM,+BAA+B,CAAA;AAaxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAEjE,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,oBAAoB,EAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAoP9B"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle agent session 'prompted' events — follow-up messages or stop/continue signals.
|
|
3
|
+
*/
|
|
4
|
+
import { NextResponse } from 'next/server';
|
|
5
|
+
import { getSessionState, updateSessionStatus, storeSessionState, releaseClaim, removeWorkerSession, dispatchWork, storePendingPrompt, generateIdempotencyKey, isWebhookProcessed, } from '@renseiai/agentfactory-server';
|
|
6
|
+
import { handleStopSignal, emitActivity } from '../utils.js';
|
|
7
|
+
export async function handleSessionPrompted(config, payload, rawPayload, log) {
|
|
8
|
+
const agentSession = rawPayload.agentSession;
|
|
9
|
+
if (!agentSession) {
|
|
10
|
+
log.warn('AgentSessionEvent prompted missing agentSession field');
|
|
11
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'missing_agent_session' });
|
|
12
|
+
}
|
|
13
|
+
const sessionId = agentSession.id;
|
|
14
|
+
const issue = agentSession.issue;
|
|
15
|
+
const issueId = agentSession.issueId || issue?.id;
|
|
16
|
+
const promptText = rawPayload.promptContext || '';
|
|
17
|
+
const webhookId = rawPayload.webhookId;
|
|
18
|
+
const user = rawPayload.user;
|
|
19
|
+
const agentActivity = rawPayload.agentActivity;
|
|
20
|
+
const comment = rawPayload.comment;
|
|
21
|
+
const commentBody = comment?.body || '';
|
|
22
|
+
const promptLog = log.child({ sessionId, issueId });
|
|
23
|
+
// Check for stop/continue signals
|
|
24
|
+
const activitySignal = agentActivity?.signal;
|
|
25
|
+
const isStopSignal = activitySignal === 'stop';
|
|
26
|
+
const isContinueSignal = activitySignal === 'continue';
|
|
27
|
+
promptLog.info('Agent session prompted', {
|
|
28
|
+
hasPromptContext: !!promptText,
|
|
29
|
+
promptContextLength: promptText?.length,
|
|
30
|
+
userName: user?.name,
|
|
31
|
+
hasAgentActivity: !!agentActivity,
|
|
32
|
+
activitySignal,
|
|
33
|
+
isStopSignal,
|
|
34
|
+
isContinueSignal,
|
|
35
|
+
hasComment: !!comment,
|
|
36
|
+
commentBodyLength: commentBody?.length,
|
|
37
|
+
});
|
|
38
|
+
// Handle stop signal
|
|
39
|
+
if (isStopSignal) {
|
|
40
|
+
promptLog.info('Stop signal received via prompted webhook');
|
|
41
|
+
await handleStopSignal(config, sessionId, issueId, payload.organizationId);
|
|
42
|
+
return NextResponse.json({ success: true, action: 'stopped', sessionId });
|
|
43
|
+
}
|
|
44
|
+
// Handle continue signal
|
|
45
|
+
if (isContinueSignal) {
|
|
46
|
+
promptLog.info('Continue signal received via prompted webhook');
|
|
47
|
+
const existingSession = await getSessionState(sessionId);
|
|
48
|
+
const workType = existingSession?.workType || 'inflight';
|
|
49
|
+
let resumePrompt = promptText || commentBody || '';
|
|
50
|
+
const issueIdentifier = existingSession?.issueIdentifier ||
|
|
51
|
+
issue?.identifier ||
|
|
52
|
+
issueId.slice(0, 8);
|
|
53
|
+
promptLog.info('Session state for continue', {
|
|
54
|
+
hasExistingSession: !!existingSession,
|
|
55
|
+
sessionStatus: existingSession?.status,
|
|
56
|
+
workType,
|
|
57
|
+
});
|
|
58
|
+
if (!resumePrompt.trim()) {
|
|
59
|
+
resumePrompt = config.generatePrompt(issueIdentifier, workType);
|
|
60
|
+
}
|
|
61
|
+
// Reset session status if in terminal state
|
|
62
|
+
if (existingSession && ['completed', 'failed', 'stopped'].includes(existingSession.status)) {
|
|
63
|
+
await releaseClaim(sessionId);
|
|
64
|
+
if (existingSession.workerId) {
|
|
65
|
+
await removeWorkerSession(existingSession.workerId, sessionId);
|
|
66
|
+
}
|
|
67
|
+
await updateSessionStatus(sessionId, 'pending');
|
|
68
|
+
}
|
|
69
|
+
// Update organizationId if missing
|
|
70
|
+
if (existingSession && !existingSession.organizationId && payload.organizationId) {
|
|
71
|
+
await storeSessionState(sessionId, {
|
|
72
|
+
...existingSession,
|
|
73
|
+
organizationId: payload.organizationId,
|
|
74
|
+
status: 'pending',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Queue work to resume
|
|
78
|
+
const work = {
|
|
79
|
+
sessionId,
|
|
80
|
+
issueId,
|
|
81
|
+
issueIdentifier,
|
|
82
|
+
priority: 2,
|
|
83
|
+
queuedAt: Date.now(),
|
|
84
|
+
prompt: resumePrompt,
|
|
85
|
+
providerSessionId: existingSession?.providerSessionId || undefined,
|
|
86
|
+
workType,
|
|
87
|
+
projectName: existingSession?.projectName,
|
|
88
|
+
};
|
|
89
|
+
await dispatchWork(work);
|
|
90
|
+
try {
|
|
91
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
92
|
+
await emitActivity(linearClient, sessionId, 'thought', 'Session resume requested. Waiting for an available worker...');
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
promptLog.error('Failed to emit continue acknowledgment activity', { error: err });
|
|
96
|
+
}
|
|
97
|
+
return NextResponse.json({ success: true, action: 'continue_queued', sessionId });
|
|
98
|
+
}
|
|
99
|
+
// Generate idempotency key for prompted events
|
|
100
|
+
const idempotencyKey = generateIdempotencyKey(webhookId, `${sessionId}:prompt:${payload.createdAt}`);
|
|
101
|
+
if (await isWebhookProcessed(idempotencyKey)) {
|
|
102
|
+
promptLog.info('Duplicate prompted webhook ignored', { idempotencyKey });
|
|
103
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'duplicate_prompt' });
|
|
104
|
+
}
|
|
105
|
+
const existingSession = await getSessionState(sessionId);
|
|
106
|
+
const issueIdentifier = existingSession?.issueIdentifier ||
|
|
107
|
+
issue?.identifier ||
|
|
108
|
+
issueId.slice(0, 8);
|
|
109
|
+
// Determine effective prompt with cascading fallbacks
|
|
110
|
+
let effectivePrompt = promptText.trim();
|
|
111
|
+
if (!effectivePrompt && commentBody.trim()) {
|
|
112
|
+
effectivePrompt = commentBody.trim();
|
|
113
|
+
promptLog.info('Using comment body as prompt fallback');
|
|
114
|
+
}
|
|
115
|
+
if (!effectivePrompt) {
|
|
116
|
+
if (existingSession) {
|
|
117
|
+
effectivePrompt = config.generatePrompt(issueIdentifier, existingSession.workType || 'inflight');
|
|
118
|
+
promptLog.info('Generated continue prompt for empty prompt');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
promptLog.warn('Empty prompt with no existing session, skipping');
|
|
122
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'empty_prompt_no_session' });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// If session is running, store as pending prompt
|
|
126
|
+
if (existingSession?.status === 'running' || existingSession?.status === 'claimed') {
|
|
127
|
+
const userInfo = user ? {
|
|
128
|
+
id: user.id,
|
|
129
|
+
name: user.name,
|
|
130
|
+
} : undefined;
|
|
131
|
+
const pendingPrompt = await storePendingPrompt(sessionId, issueId, effectivePrompt, userInfo);
|
|
132
|
+
if (pendingPrompt) {
|
|
133
|
+
promptLog.info('Pending prompt stored for running session', {
|
|
134
|
+
promptId: pendingPrompt.id,
|
|
135
|
+
sessionId,
|
|
136
|
+
issueIdentifier,
|
|
137
|
+
workerId: existingSession.workerId,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
promptLog.error('Failed to store pending prompt');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Session not running — queue as work
|
|
146
|
+
promptLog.info('Queuing follow-up for non-running session', {
|
|
147
|
+
sessionStatus: existingSession?.status,
|
|
148
|
+
});
|
|
149
|
+
if (existingSession && ['completed', 'failed', 'stopped'].includes(existingSession.status)) {
|
|
150
|
+
await releaseClaim(sessionId);
|
|
151
|
+
if (existingSession.workerId) {
|
|
152
|
+
await removeWorkerSession(existingSession.workerId, sessionId);
|
|
153
|
+
}
|
|
154
|
+
await updateSessionStatus(sessionId, 'pending');
|
|
155
|
+
}
|
|
156
|
+
if (existingSession && !existingSession.organizationId && payload.organizationId) {
|
|
157
|
+
await storeSessionState(sessionId, {
|
|
158
|
+
...existingSession,
|
|
159
|
+
organizationId: payload.organizationId,
|
|
160
|
+
status: 'pending',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const work = {
|
|
164
|
+
sessionId,
|
|
165
|
+
issueId,
|
|
166
|
+
issueIdentifier,
|
|
167
|
+
priority: 2,
|
|
168
|
+
queuedAt: Date.now(),
|
|
169
|
+
prompt: effectivePrompt,
|
|
170
|
+
providerSessionId: existingSession?.providerSessionId || undefined,
|
|
171
|
+
workType: existingSession?.workType || 'inflight',
|
|
172
|
+
projectName: existingSession?.projectName,
|
|
173
|
+
};
|
|
174
|
+
const dispatchResult = await dispatchWork(work);
|
|
175
|
+
if (dispatchResult.dispatched || dispatchResult.parked) {
|
|
176
|
+
promptLog.info('Follow-up prompt dispatched', {
|
|
177
|
+
sessionId,
|
|
178
|
+
issueIdentifier,
|
|
179
|
+
dispatched: dispatchResult.dispatched,
|
|
180
|
+
parked: dispatchResult.parked,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
promptLog.error('Failed to dispatch follow-up prompt');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Acknowledge prompt receipt
|
|
188
|
+
try {
|
|
189
|
+
const linearClient = await config.linearClient.getClient(payload.organizationId);
|
|
190
|
+
const truncatedPrompt = effectivePrompt.length > 100
|
|
191
|
+
? `${effectivePrompt.substring(0, 100)}...`
|
|
192
|
+
: effectivePrompt;
|
|
193
|
+
await emitActivity(linearClient, sessionId, 'thought', `Follow-up received: "${truncatedPrompt}" - Processing...`);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
promptLog.error('Failed to emit prompt acknowledgment activity', { error: err });
|
|
197
|
+
}
|
|
198
|
+
return null; // Continue processing
|
|
199
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle agent session 'update' events — stop signal from user.
|
|
3
|
+
*/
|
|
4
|
+
import { NextResponse } from 'next/server';
|
|
5
|
+
import type { LinearWebhookPayload } from '@renseiai/agentfactory-linear';
|
|
6
|
+
import type { ResolvedWebhookConfig } from '../../types.js';
|
|
7
|
+
import type { createLogger } from '@renseiai/agentfactory-server';
|
|
8
|
+
export declare function handleSessionUpdated(config: ResolvedWebhookConfig, payload: LinearWebhookPayload, rawPayload: Record<string, unknown>, log: ReturnType<typeof createLogger>): Promise<NextResponse | null>;
|
|
9
|
+
//# sourceMappingURL=session-updated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-updated.d.ts","sourceRoot":"","sources":["../../../../src/webhook/handlers/session-updated.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAEjE,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,oBAAoB,EAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,GAAG,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA6B9B"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle agent session 'update' events — stop signal from user.
|
|
3
|
+
*/
|
|
4
|
+
import { NextResponse } from 'next/server';
|
|
5
|
+
import { handleStopSignal } from '../utils.js';
|
|
6
|
+
export async function handleSessionUpdated(config, payload, rawPayload, log) {
|
|
7
|
+
const agentSession = rawPayload.agentSession;
|
|
8
|
+
if (!agentSession) {
|
|
9
|
+
log.warn('AgentSessionEvent updated missing agentSession field');
|
|
10
|
+
return NextResponse.json({ success: true, skipped: true, reason: 'missing_agent_session' });
|
|
11
|
+
}
|
|
12
|
+
const sessionId = agentSession.id;
|
|
13
|
+
const issue = agentSession.issue;
|
|
14
|
+
const issueId = agentSession.issueId || issue?.id;
|
|
15
|
+
const newState = agentSession.state;
|
|
16
|
+
const updatedFrom = rawPayload.updatedFrom;
|
|
17
|
+
const previousState = updatedFrom?.state;
|
|
18
|
+
const updateLog = log.child({ sessionId, issueId });
|
|
19
|
+
updateLog.info('Agent session updated', {
|
|
20
|
+
newState,
|
|
21
|
+
previousState,
|
|
22
|
+
});
|
|
23
|
+
// Check if this is a stop signal (state changed to completed/failed)
|
|
24
|
+
if (newState === 'completed' || newState === 'failed') {
|
|
25
|
+
updateLog.info('Stop signal received via updated webhook', { previousState });
|
|
26
|
+
await handleStopSignal(config, sessionId, issueId, payload.organizationId);
|
|
27
|
+
}
|
|
28
|
+
return null; // Continue processing other handlers
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Processor — Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Receives Linear webhook events, verifies signatures, runs idempotency
|
|
5
|
+
* checks, and dispatches to sub-handlers.
|
|
6
|
+
*/
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
8
|
+
import type { ResolvedWebhookConfig } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Create webhook route handlers from config.
|
|
11
|
+
*
|
|
12
|
+
* Returns { POST, GET } for use as Next.js App Router exports.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createWebhookHandler(config: ResolvedWebhookConfig): {
|
|
15
|
+
POST: (request: NextRequest) => Promise<NextResponse<unknown>>;
|
|
16
|
+
GET: () => Promise<NextResponse<{
|
|
17
|
+
status: string;
|
|
18
|
+
endpoint: string;
|
|
19
|
+
description: string;
|
|
20
|
+
}>>;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=processor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processor.d.ts","sourceRoot":"","sources":["../../../src/webhook/processor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AASvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AASxD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,qBAAqB;oBACnC,WAAW;;;;;;EA2FzC"}
|