patchrelay 0.12.9 → 0.14.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/dist/agent-session-plan.js +155 -11
- package/dist/agent-session-presentation.js +19 -14
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +1 -0
- package/dist/cli/commands/watch.js +30 -0
- package/dist/cli/help.js +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/watch/App.js +47 -0
- package/dist/cli/watch/HelpBar.js +7 -0
- package/dist/cli/watch/IssueDetailView.js +18 -0
- package/dist/cli/watch/IssueListView.js +8 -0
- package/dist/cli/watch/IssueRow.js +59 -0
- package/dist/cli/watch/ItemLine.js +72 -0
- package/dist/cli/watch/StatusBar.js +11 -0
- package/dist/cli/watch/ThreadView.js +20 -0
- package/dist/cli/watch/TurnSection.js +15 -0
- package/dist/cli/watch/use-detail-stream.js +193 -0
- package/dist/cli/watch/use-watch-stream.js +102 -0
- package/dist/cli/watch/watch-state.js +285 -0
- package/dist/codex-app-server.js +1 -4
- package/dist/config.js +2 -0
- package/dist/github-webhook-handler.js +33 -17
- package/dist/http.js +81 -0
- package/dist/issue-query-service.js +17 -2
- package/dist/linear-session-reporting.js +134 -0
- package/dist/run-orchestrator.js +54 -17
- package/dist/run-reporting.js +31 -0
- package/dist/service.js +60 -0
- package/dist/webhook-handler.js +49 -28
- package/package.json +4 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
export function useDetailStream(options) {
|
|
3
|
+
const optionsRef = useRef(options);
|
|
4
|
+
optionsRef.current = options;
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const { issueKey } = optionsRef.current;
|
|
7
|
+
if (!issueKey)
|
|
8
|
+
return;
|
|
9
|
+
const abortController = new AbortController();
|
|
10
|
+
const { baseUrl, bearerToken, dispatch } = optionsRef.current;
|
|
11
|
+
const headers = { accept: "application/json" };
|
|
12
|
+
if (bearerToken) {
|
|
13
|
+
headers.authorization = `Bearer ${bearerToken}`;
|
|
14
|
+
}
|
|
15
|
+
// Rehydrate from thread/read via /api/issues/:key/live
|
|
16
|
+
void rehydrate(baseUrl, issueKey, headers, abortController.signal, dispatch);
|
|
17
|
+
// Stream codex notifications via filtered SSE
|
|
18
|
+
void streamCodexEvents(baseUrl, issueKey, headers, abortController.signal, dispatch);
|
|
19
|
+
return () => {
|
|
20
|
+
abortController.abort();
|
|
21
|
+
};
|
|
22
|
+
}, [options.issueKey]);
|
|
23
|
+
}
|
|
24
|
+
async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}/live`, baseUrl);
|
|
27
|
+
const response = await fetch(url, { headers, signal });
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
return;
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
const threadData = data.thread;
|
|
32
|
+
if (threadData) {
|
|
33
|
+
dispatch({ type: "thread-snapshot", thread: materializeThread(threadData) });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// No active thread — fall back to latest run report
|
|
37
|
+
await rehydrateFromReport(baseUrl, issueKey, headers, signal, dispatch);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Rehydration is best-effort — SSE stream will provide updates
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function rehydrateFromReport(baseUrl, issueKey, headers, signal, dispatch) {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}/report`, baseUrl);
|
|
46
|
+
const response = await fetch(url, { headers, signal });
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
return;
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
const latest = data.runs?.[0];
|
|
51
|
+
if (!latest)
|
|
52
|
+
return;
|
|
53
|
+
const report = {
|
|
54
|
+
runType: latest.run.runType,
|
|
55
|
+
status: latest.run.status,
|
|
56
|
+
summary: typeof latest.summary?.latestAssistantMessage === "string"
|
|
57
|
+
? latest.summary.latestAssistantMessage
|
|
58
|
+
: latest.report?.assistantMessages.at(-1),
|
|
59
|
+
commands: latest.report?.commands.map((c) => ({
|
|
60
|
+
command: c.command,
|
|
61
|
+
...(typeof c.exitCode === "number" ? { exitCode: c.exitCode } : {}),
|
|
62
|
+
...(typeof c.durationMs === "number" ? { durationMs: c.durationMs } : {}),
|
|
63
|
+
})) ?? [],
|
|
64
|
+
fileChanges: latest.report?.fileChanges.length ?? 0,
|
|
65
|
+
toolCalls: latest.report?.toolCalls.length ?? 0,
|
|
66
|
+
assistantMessages: latest.report?.assistantMessages ?? [],
|
|
67
|
+
};
|
|
68
|
+
dispatch({ type: "report-snapshot", report });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Report fetch is best-effort
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function streamCodexEvents(baseUrl, issueKey, baseHeaders, signal, dispatch) {
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL("/api/watch", baseUrl);
|
|
77
|
+
url.searchParams.set("issue", issueKey);
|
|
78
|
+
const headers = { ...baseHeaders, accept: "text/event-stream" };
|
|
79
|
+
const response = await fetch(url, { headers, signal });
|
|
80
|
+
if (!response.ok || !response.body)
|
|
81
|
+
return;
|
|
82
|
+
const reader = response.body.getReader();
|
|
83
|
+
const decoder = new TextDecoder();
|
|
84
|
+
let buffer = "";
|
|
85
|
+
let eventType = "";
|
|
86
|
+
let dataLines = [];
|
|
87
|
+
while (true) {
|
|
88
|
+
const { done, value } = await reader.read();
|
|
89
|
+
if (done)
|
|
90
|
+
break;
|
|
91
|
+
buffer += decoder.decode(value, { stream: true });
|
|
92
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
93
|
+
while (newlineIndex !== -1) {
|
|
94
|
+
const rawLine = buffer.slice(0, newlineIndex);
|
|
95
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
96
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
97
|
+
if (!line) {
|
|
98
|
+
if (dataLines.length > 0) {
|
|
99
|
+
processDetailEvent(dispatch, eventType, dataLines.join("\n"));
|
|
100
|
+
dataLines = [];
|
|
101
|
+
eventType = "";
|
|
102
|
+
}
|
|
103
|
+
newlineIndex = buffer.indexOf("\n");
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (line.startsWith(":")) {
|
|
107
|
+
newlineIndex = buffer.indexOf("\n");
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (line.startsWith("event:")) {
|
|
111
|
+
eventType = line.slice(6).trim();
|
|
112
|
+
}
|
|
113
|
+
else if (line.startsWith("data:")) {
|
|
114
|
+
dataLines.push(line.slice(5).trimStart());
|
|
115
|
+
}
|
|
116
|
+
newlineIndex = buffer.indexOf("\n");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Stream ended or aborted
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function processDetailEvent(dispatch, eventType, data) {
|
|
125
|
+
try {
|
|
126
|
+
if (eventType === "codex") {
|
|
127
|
+
const parsed = JSON.parse(data);
|
|
128
|
+
dispatch({ type: "codex-notification", method: parsed.method, params: parsed.params });
|
|
129
|
+
}
|
|
130
|
+
// Feed events are already handled by the main watch stream
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Ignore parse errors
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ─── Thread Materialization from thread/read ──────────────────────
|
|
137
|
+
function materializeThread(summary) {
|
|
138
|
+
return {
|
|
139
|
+
threadId: summary.id,
|
|
140
|
+
status: summary.status,
|
|
141
|
+
turns: summary.turns.map(materializeTurn),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function materializeTurn(turn) {
|
|
145
|
+
return {
|
|
146
|
+
id: turn.id,
|
|
147
|
+
status: turn.status,
|
|
148
|
+
items: turn.items.map(materializeItem),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function materializeItem(item) {
|
|
152
|
+
// CodexThreadItem has an index-signature catch-all that defeats narrowing.
|
|
153
|
+
// Access fields via Record<string, unknown> and coerce explicitly.
|
|
154
|
+
const r = item;
|
|
155
|
+
const id = String(r.id ?? "unknown");
|
|
156
|
+
const type = String(r.type ?? "unknown");
|
|
157
|
+
const base = { id, type, status: "completed" };
|
|
158
|
+
switch (type) {
|
|
159
|
+
case "agentMessage":
|
|
160
|
+
return { ...base, text: String(r.text ?? "") };
|
|
161
|
+
case "commandExecution":
|
|
162
|
+
return {
|
|
163
|
+
...base,
|
|
164
|
+
command: String(r.command ?? ""),
|
|
165
|
+
status: String(r.status ?? "completed"),
|
|
166
|
+
...(typeof r.exitCode === "number" ? { exitCode: r.exitCode } : {}),
|
|
167
|
+
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
168
|
+
...(typeof r.aggregatedOutput === "string" ? { output: r.aggregatedOutput } : {}),
|
|
169
|
+
};
|
|
170
|
+
case "fileChange":
|
|
171
|
+
return { ...base, status: String(r.status ?? "completed"), changes: Array.isArray(r.changes) ? r.changes : [] };
|
|
172
|
+
case "mcpToolCall":
|
|
173
|
+
return {
|
|
174
|
+
...base,
|
|
175
|
+
status: String(r.status ?? "completed"),
|
|
176
|
+
toolName: `${String(r.server ?? "")}/${String(r.tool ?? "")}`,
|
|
177
|
+
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
178
|
+
};
|
|
179
|
+
case "dynamicToolCall":
|
|
180
|
+
return {
|
|
181
|
+
...base,
|
|
182
|
+
status: String(r.status ?? "completed"),
|
|
183
|
+
toolName: String(r.tool ?? ""),
|
|
184
|
+
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
185
|
+
};
|
|
186
|
+
case "plan":
|
|
187
|
+
return { ...base, text: String(r.text ?? "") };
|
|
188
|
+
case "reasoning":
|
|
189
|
+
return { ...base, text: Array.isArray(r.summary) ? r.summary.join("\n") : "" };
|
|
190
|
+
default:
|
|
191
|
+
return base;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
export function useWatchStream(options) {
|
|
3
|
+
const optionsRef = useRef(options);
|
|
4
|
+
optionsRef.current = options;
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
let abortController = new AbortController();
|
|
7
|
+
let reconnectTimeout;
|
|
8
|
+
let attempt = 0;
|
|
9
|
+
const connect = () => {
|
|
10
|
+
abortController = new AbortController();
|
|
11
|
+
const { baseUrl, bearerToken, issueFilter, dispatch } = optionsRef.current;
|
|
12
|
+
const url = new URL("/api/watch", baseUrl);
|
|
13
|
+
if (issueFilter) {
|
|
14
|
+
url.searchParams.set("issue", issueFilter);
|
|
15
|
+
}
|
|
16
|
+
const headers = { accept: "text/event-stream" };
|
|
17
|
+
if (bearerToken) {
|
|
18
|
+
headers.authorization = `Bearer ${bearerToken}`;
|
|
19
|
+
}
|
|
20
|
+
void fetch(url, { headers, signal: abortController.signal })
|
|
21
|
+
.then(async (response) => {
|
|
22
|
+
if (!response.ok || !response.body) {
|
|
23
|
+
throw new Error(`Watch stream failed: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
dispatch({ type: "connected" });
|
|
26
|
+
attempt = 0;
|
|
27
|
+
const reader = response.body.getReader();
|
|
28
|
+
const decoder = new TextDecoder();
|
|
29
|
+
let buffer = "";
|
|
30
|
+
let eventType = "";
|
|
31
|
+
let dataLines = [];
|
|
32
|
+
while (true) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done)
|
|
35
|
+
break;
|
|
36
|
+
buffer += decoder.decode(value, { stream: true });
|
|
37
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
38
|
+
while (newlineIndex !== -1) {
|
|
39
|
+
const rawLine = buffer.slice(0, newlineIndex);
|
|
40
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
41
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
42
|
+
if (!line) {
|
|
43
|
+
if (dataLines.length > 0) {
|
|
44
|
+
processEvent(dispatch, eventType, dataLines.join("\n"));
|
|
45
|
+
dataLines = [];
|
|
46
|
+
eventType = "";
|
|
47
|
+
}
|
|
48
|
+
newlineIndex = buffer.indexOf("\n");
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (line.startsWith(":")) {
|
|
52
|
+
newlineIndex = buffer.indexOf("\n");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (line.startsWith("event:")) {
|
|
56
|
+
eventType = line.slice(6).trim();
|
|
57
|
+
}
|
|
58
|
+
else if (line.startsWith("data:")) {
|
|
59
|
+
dataLines.push(line.slice(5).trimStart());
|
|
60
|
+
}
|
|
61
|
+
newlineIndex = buffer.indexOf("\n");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.catch((error) => {
|
|
66
|
+
if (abortController.signal.aborted)
|
|
67
|
+
return;
|
|
68
|
+
const _msg = error instanceof Error ? error.message : String(error);
|
|
69
|
+
})
|
|
70
|
+
.finally(() => {
|
|
71
|
+
if (abortController.signal.aborted)
|
|
72
|
+
return;
|
|
73
|
+
dispatch({ type: "disconnected" });
|
|
74
|
+
attempt = Math.min(attempt + 1, 5);
|
|
75
|
+
const delay = Math.min(2000 * Math.pow(2, attempt), 30000);
|
|
76
|
+
reconnectTimeout = setTimeout(connect, delay);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
connect();
|
|
80
|
+
return () => {
|
|
81
|
+
abortController.abort();
|
|
82
|
+
if (reconnectTimeout !== undefined) {
|
|
83
|
+
clearTimeout(reconnectTimeout);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}, []);
|
|
87
|
+
}
|
|
88
|
+
function processEvent(dispatch, eventType, data) {
|
|
89
|
+
try {
|
|
90
|
+
if (eventType === "issues") {
|
|
91
|
+
const issues = JSON.parse(data);
|
|
92
|
+
dispatch({ type: "issues-snapshot", issues });
|
|
93
|
+
}
|
|
94
|
+
else if (eventType === "feed") {
|
|
95
|
+
const event = JSON.parse(data);
|
|
96
|
+
dispatch({ type: "feed-event", event });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Ignore parse errors from malformed events
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
export const initialWatchState = {
|
|
2
|
+
connected: false,
|
|
3
|
+
issues: [],
|
|
4
|
+
selectedIndex: 0,
|
|
5
|
+
view: "list",
|
|
6
|
+
activeDetailKey: null,
|
|
7
|
+
thread: null,
|
|
8
|
+
report: null,
|
|
9
|
+
filter: "non-done",
|
|
10
|
+
};
|
|
11
|
+
const TERMINAL_FACTORY_STATES = new Set(["done", "failed"]);
|
|
12
|
+
export function filterIssues(issues, filter) {
|
|
13
|
+
switch (filter) {
|
|
14
|
+
case "all":
|
|
15
|
+
return issues;
|
|
16
|
+
case "active":
|
|
17
|
+
return issues.filter((i) => i.activeRunType !== undefined);
|
|
18
|
+
case "non-done":
|
|
19
|
+
return issues.filter((i) => !TERMINAL_FACTORY_STATES.has(i.factoryState));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function nextFilter(filter) {
|
|
23
|
+
switch (filter) {
|
|
24
|
+
case "non-done": return "active";
|
|
25
|
+
case "active": return "all";
|
|
26
|
+
case "all": return "non-done";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function watchReducer(state, action) {
|
|
30
|
+
switch (action.type) {
|
|
31
|
+
case "connected":
|
|
32
|
+
return { ...state, connected: true };
|
|
33
|
+
case "disconnected":
|
|
34
|
+
return { ...state, connected: false };
|
|
35
|
+
case "issues-snapshot":
|
|
36
|
+
return {
|
|
37
|
+
...state,
|
|
38
|
+
issues: action.issues,
|
|
39
|
+
selectedIndex: Math.min(state.selectedIndex, Math.max(0, action.issues.length - 1)),
|
|
40
|
+
};
|
|
41
|
+
case "feed-event":
|
|
42
|
+
return applyFeedEvent(state, action.event);
|
|
43
|
+
case "select":
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
selectedIndex: Math.max(0, Math.min(action.index, state.issues.length - 1)),
|
|
47
|
+
};
|
|
48
|
+
case "enter-detail":
|
|
49
|
+
return { ...state, view: "detail", activeDetailKey: action.issueKey, thread: null, report: null };
|
|
50
|
+
case "exit-detail":
|
|
51
|
+
return { ...state, view: "list", activeDetailKey: null, thread: null, report: null };
|
|
52
|
+
case "thread-snapshot":
|
|
53
|
+
return { ...state, thread: action.thread };
|
|
54
|
+
case "report-snapshot":
|
|
55
|
+
return { ...state, report: action.report };
|
|
56
|
+
case "codex-notification":
|
|
57
|
+
return applyCodexNotification(state, action.method, action.params);
|
|
58
|
+
case "cycle-filter":
|
|
59
|
+
return { ...state, filter: nextFilter(state.filter), selectedIndex: 0 };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ─── Feed Event Application ───────────────────────────────────────
|
|
63
|
+
function applyFeedEvent(state, event) {
|
|
64
|
+
if (!event.issueKey) {
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
const index = state.issues.findIndex((issue) => issue.issueKey === event.issueKey);
|
|
68
|
+
if (index === -1) {
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
const updated = [...state.issues];
|
|
72
|
+
const issue = { ...updated[index] };
|
|
73
|
+
if (event.kind === "stage" && event.stage) {
|
|
74
|
+
issue.factoryState = event.stage;
|
|
75
|
+
}
|
|
76
|
+
if (event.kind === "stage" && event.status === "starting" && event.stage) {
|
|
77
|
+
issue.activeRunType = event.stage;
|
|
78
|
+
}
|
|
79
|
+
if (event.kind === "turn") {
|
|
80
|
+
if (event.status === "completed" || event.status === "failed") {
|
|
81
|
+
issue.activeRunType = undefined;
|
|
82
|
+
issue.latestRunStatus = event.status;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (event.kind === "github" && event.status) {
|
|
86
|
+
if (event.status === "check_passed" || event.status === "check_failed") {
|
|
87
|
+
issue.prCheckStatus = event.status === "check_passed" ? "passed" : "failed";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
issue.updatedAt = event.at;
|
|
91
|
+
updated[index] = issue;
|
|
92
|
+
return { ...state, issues: updated };
|
|
93
|
+
}
|
|
94
|
+
// ─── Codex Notification Application ───────────────────────────────
|
|
95
|
+
function applyCodexNotification(state, method, params) {
|
|
96
|
+
if (!state.thread) {
|
|
97
|
+
// No thread loaded yet — only turn/started can bootstrap one
|
|
98
|
+
if (method === "turn/started") {
|
|
99
|
+
return bootstrapThreadFromTurnStarted(state, params);
|
|
100
|
+
}
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
switch (method) {
|
|
104
|
+
case "turn/started":
|
|
105
|
+
return withThread(state, addTurn(state.thread, params));
|
|
106
|
+
case "turn/completed":
|
|
107
|
+
return withThread(state, completeTurn(state.thread, params));
|
|
108
|
+
case "turn/plan/updated":
|
|
109
|
+
return withThread(state, updatePlan(state.thread, params));
|
|
110
|
+
case "turn/diff/updated":
|
|
111
|
+
return withThread(state, updateDiff(state.thread, params));
|
|
112
|
+
case "item/started":
|
|
113
|
+
return withThread(state, addItem(state.thread, params));
|
|
114
|
+
case "item/completed":
|
|
115
|
+
return withThread(state, completeItem(state.thread, params));
|
|
116
|
+
case "item/agentMessage/delta":
|
|
117
|
+
return withThread(state, appendItemText(state.thread, params));
|
|
118
|
+
case "item/commandExecution/outputDelta":
|
|
119
|
+
return withThread(state, appendItemOutput(state.thread, params));
|
|
120
|
+
case "item/plan/delta":
|
|
121
|
+
return withThread(state, appendItemText(state.thread, params));
|
|
122
|
+
case "item/reasoning/summaryTextDelta":
|
|
123
|
+
return withThread(state, appendItemText(state.thread, params));
|
|
124
|
+
case "thread/status/changed":
|
|
125
|
+
return withThread(state, updateThreadStatus(state.thread, params));
|
|
126
|
+
default:
|
|
127
|
+
return state;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function withThread(state, thread) {
|
|
131
|
+
return { ...state, thread };
|
|
132
|
+
}
|
|
133
|
+
function bootstrapThreadFromTurnStarted(state, params) {
|
|
134
|
+
const turnObj = params.turn;
|
|
135
|
+
const threadId = typeof params.threadId === "string" ? params.threadId : "unknown";
|
|
136
|
+
const turnId = typeof turnObj?.id === "string" ? turnObj.id : "unknown";
|
|
137
|
+
return {
|
|
138
|
+
...state,
|
|
139
|
+
thread: {
|
|
140
|
+
threadId,
|
|
141
|
+
status: "active",
|
|
142
|
+
turns: [{ id: turnId, status: "inProgress", items: [] }],
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// ─── Turn Handlers ────────────────────────────────────────────────
|
|
147
|
+
function addTurn(thread, params) {
|
|
148
|
+
const turnObj = params.turn;
|
|
149
|
+
const turnId = typeof turnObj?.id === "string" ? turnObj.id : "unknown";
|
|
150
|
+
const existing = thread.turns.find((t) => t.id === turnId);
|
|
151
|
+
if (existing) {
|
|
152
|
+
return thread;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
...thread,
|
|
156
|
+
status: "active",
|
|
157
|
+
turns: [...thread.turns, { id: turnId, status: "inProgress", items: [] }],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function completeTurn(thread, params) {
|
|
161
|
+
const turnObj = params.turn;
|
|
162
|
+
const turnId = typeof turnObj?.id === "string" ? turnObj.id : undefined;
|
|
163
|
+
const status = typeof turnObj?.status === "string" ? turnObj.status : "completed";
|
|
164
|
+
if (!turnId)
|
|
165
|
+
return thread;
|
|
166
|
+
return {
|
|
167
|
+
...thread,
|
|
168
|
+
turns: thread.turns.map((t) => t.id === turnId ? { ...t, status } : t),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function updatePlan(thread, params) {
|
|
172
|
+
const plan = params.plan;
|
|
173
|
+
if (!Array.isArray(plan))
|
|
174
|
+
return thread;
|
|
175
|
+
return {
|
|
176
|
+
...thread,
|
|
177
|
+
plan: plan.map((entry) => {
|
|
178
|
+
const e = entry;
|
|
179
|
+
return {
|
|
180
|
+
step: typeof e.step === "string" ? e.step : String(e.step ?? ""),
|
|
181
|
+
status: typeof e.status === "string" ? e.status : "pending",
|
|
182
|
+
};
|
|
183
|
+
}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function updateDiff(thread, params) {
|
|
187
|
+
const diff = typeof params.diff === "string" ? params.diff : undefined;
|
|
188
|
+
return { ...thread, diff };
|
|
189
|
+
}
|
|
190
|
+
function updateThreadStatus(thread, params) {
|
|
191
|
+
const statusObj = params.status;
|
|
192
|
+
const statusType = typeof statusObj?.type === "string" ? statusObj.type : undefined;
|
|
193
|
+
if (!statusType)
|
|
194
|
+
return thread;
|
|
195
|
+
return { ...thread, status: statusType };
|
|
196
|
+
}
|
|
197
|
+
// ─── Item Handlers ────────────────────────────────────────────────
|
|
198
|
+
function getLatestTurn(thread) {
|
|
199
|
+
return thread.turns[thread.turns.length - 1];
|
|
200
|
+
}
|
|
201
|
+
function updateLatestTurn(thread, updater) {
|
|
202
|
+
const last = getLatestTurn(thread);
|
|
203
|
+
if (!last)
|
|
204
|
+
return thread;
|
|
205
|
+
return {
|
|
206
|
+
...thread,
|
|
207
|
+
turns: [...thread.turns.slice(0, -1), updater(last)],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function addItem(thread, params) {
|
|
211
|
+
const itemObj = params.item;
|
|
212
|
+
if (!itemObj)
|
|
213
|
+
return thread;
|
|
214
|
+
const id = typeof itemObj.id === "string" ? itemObj.id : "unknown";
|
|
215
|
+
const type = typeof itemObj.type === "string" ? itemObj.type : "unknown";
|
|
216
|
+
const status = typeof itemObj.status === "string" ? itemObj.status : "inProgress";
|
|
217
|
+
const item = { id, type, status };
|
|
218
|
+
if (type === "agentMessage" && typeof itemObj.text === "string") {
|
|
219
|
+
item.text = itemObj.text;
|
|
220
|
+
}
|
|
221
|
+
if (type === "commandExecution") {
|
|
222
|
+
const cmd = itemObj.command;
|
|
223
|
+
item.command = Array.isArray(cmd) ? cmd.join(" ") : typeof cmd === "string" ? cmd : undefined;
|
|
224
|
+
}
|
|
225
|
+
if (type === "mcpToolCall") {
|
|
226
|
+
const server = typeof itemObj.server === "string" ? itemObj.server : "";
|
|
227
|
+
const tool = typeof itemObj.tool === "string" ? itemObj.tool : "";
|
|
228
|
+
item.toolName = `${server}/${tool}`;
|
|
229
|
+
}
|
|
230
|
+
if (type === "dynamicToolCall") {
|
|
231
|
+
item.toolName = typeof itemObj.tool === "string" ? itemObj.tool : undefined;
|
|
232
|
+
}
|
|
233
|
+
return updateLatestTurn(thread, (turn) => ({
|
|
234
|
+
...turn,
|
|
235
|
+
items: [...turn.items, item],
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
function completeItem(thread, params) {
|
|
239
|
+
const itemObj = params.item;
|
|
240
|
+
if (!itemObj)
|
|
241
|
+
return thread;
|
|
242
|
+
const id = typeof itemObj.id === "string" ? itemObj.id : undefined;
|
|
243
|
+
if (!id)
|
|
244
|
+
return thread;
|
|
245
|
+
const status = typeof itemObj.status === "string" ? itemObj.status : "completed";
|
|
246
|
+
const exitCode = typeof itemObj.exitCode === "number" ? itemObj.exitCode : undefined;
|
|
247
|
+
const durationMs = typeof itemObj.durationMs === "number" ? itemObj.durationMs : undefined;
|
|
248
|
+
const text = typeof itemObj.text === "string" ? itemObj.text : undefined;
|
|
249
|
+
const changes = Array.isArray(itemObj.changes) ? itemObj.changes : undefined;
|
|
250
|
+
return updateLatestTurn(thread, (turn) => ({
|
|
251
|
+
...turn,
|
|
252
|
+
items: turn.items.map((item) => {
|
|
253
|
+
if (item.id !== id)
|
|
254
|
+
return item;
|
|
255
|
+
return {
|
|
256
|
+
...item,
|
|
257
|
+
status,
|
|
258
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
259
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
260
|
+
...(text !== undefined ? { text } : {}),
|
|
261
|
+
...(changes !== undefined ? { changes } : {}),
|
|
262
|
+
};
|
|
263
|
+
}),
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
function appendItemText(thread, params) {
|
|
267
|
+
const itemId = typeof params.itemId === "string" ? params.itemId : undefined;
|
|
268
|
+
const delta = typeof params.delta === "string" ? params.delta : undefined;
|
|
269
|
+
if (!itemId || !delta)
|
|
270
|
+
return thread;
|
|
271
|
+
return updateLatestTurn(thread, (turn) => ({
|
|
272
|
+
...turn,
|
|
273
|
+
items: turn.items.map((item) => item.id === itemId ? { ...item, text: (item.text ?? "") + delta } : item),
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
function appendItemOutput(thread, params) {
|
|
277
|
+
const itemId = typeof params.itemId === "string" ? params.itemId : undefined;
|
|
278
|
+
const delta = typeof params.delta === "string" ? params.delta : undefined;
|
|
279
|
+
if (!itemId || !delta)
|
|
280
|
+
return thread;
|
|
281
|
+
return updateLatestTurn(thread, (turn) => ({
|
|
282
|
+
...turn,
|
|
283
|
+
items: turn.items.map((item) => item.id === itemId ? { ...item, output: (item.output ?? "") + delta } : item),
|
|
284
|
+
}));
|
|
285
|
+
}
|
package/dist/codex-app-server.js
CHANGED
|
@@ -141,11 +141,8 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
141
141
|
modelProvider: this.config.modelProvider ?? null,
|
|
142
142
|
baseInstructions: this.config.baseInstructions ?? null,
|
|
143
143
|
developerInstructions: this.config.developerInstructions ?? null,
|
|
144
|
-
experimentalRawEvents: false,
|
|
144
|
+
experimentalRawEvents: this.config.experimentalRawEvents ?? false,
|
|
145
145
|
};
|
|
146
|
-
if (this.config.persistExtendedHistory) {
|
|
147
|
-
this.logger.warn("persistExtendedHistory is requested but not enabled in the active app-server capability handshake; ignoring");
|
|
148
|
-
}
|
|
149
146
|
const response = (await this.sendRequest("thread/start", params));
|
|
150
147
|
return this.mapThread(response.thread);
|
|
151
148
|
}
|
package/dist/config.js
CHANGED
|
@@ -95,6 +95,7 @@ const configSchema = z.object({
|
|
|
95
95
|
approval_policy: z.enum(["never", "on-request", "on-failure", "untrusted"]).default("never"),
|
|
96
96
|
sandbox_mode: z.enum(["danger-full-access", "workspace-write", "read-only"]).default("danger-full-access"),
|
|
97
97
|
persist_extended_history: z.boolean().default(false),
|
|
98
|
+
experimental_raw_events: z.boolean().default(false),
|
|
98
99
|
}),
|
|
99
100
|
}),
|
|
100
101
|
projects: z.array(projectSchema).default([]),
|
|
@@ -369,6 +370,7 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
369
370
|
approvalPolicy: parsed.runner.codex.approval_policy,
|
|
370
371
|
sandboxMode: parsed.runner.codex.sandbox_mode,
|
|
371
372
|
persistExtendedHistory: parsed.runner.codex.persist_extended_history,
|
|
373
|
+
experimentalRawEvents: parsed.runner.codex.experimental_raw_events,
|
|
372
374
|
},
|
|
373
375
|
},
|
|
374
376
|
projects: parsed.projects.map((project) => {
|