@ottocode/server 0.1.195 → 0.1.197
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/package.json +23 -3
- package/src/runtime/agent/oauth-codex-continuation.ts +72 -0
- package/src/runtime/agent/runner-setup.ts +1 -0
- package/src/runtime/agent/runner.ts +118 -55
- package/src/runtime/message/history-builder.ts +5 -1
- package/src/runtime/prompt/builder.ts +36 -20
- package/src/runtime/stream/error-handler.ts +2 -2
- package/src/runtime/stream/text-guard.ts +77 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.197",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -14,6 +14,26 @@
|
|
|
14
14
|
"import": "./src/runtime/agent-registry.ts",
|
|
15
15
|
"types": "./src/runtime/agent-registry.ts"
|
|
16
16
|
},
|
|
17
|
+
"./runtime/ask/service": {
|
|
18
|
+
"import": "./src/runtime/ask/service.ts",
|
|
19
|
+
"types": "./src/runtime/ask/service.ts"
|
|
20
|
+
},
|
|
21
|
+
"./runtime/agent/runner": {
|
|
22
|
+
"import": "./src/runtime/agent/runner.ts",
|
|
23
|
+
"types": "./src/runtime/agent/runner.ts"
|
|
24
|
+
},
|
|
25
|
+
"./events/bus": {
|
|
26
|
+
"import": "./src/events/bus.ts",
|
|
27
|
+
"types": "./src/events/bus.ts"
|
|
28
|
+
},
|
|
29
|
+
"./events/types": {
|
|
30
|
+
"import": "./src/events/types.ts",
|
|
31
|
+
"types": "./src/events/types.ts"
|
|
32
|
+
},
|
|
33
|
+
"./runtime/tools/approval": {
|
|
34
|
+
"import": "./src/runtime/tools/approval.ts",
|
|
35
|
+
"types": "./src/runtime/tools/approval.ts"
|
|
36
|
+
},
|
|
17
37
|
"./runtime/ask-service.ts": {
|
|
18
38
|
"import": "./src/runtime/ask-service.ts",
|
|
19
39
|
"types": "./src/runtime/ask-service.ts"
|
|
@@ -29,8 +49,8 @@
|
|
|
29
49
|
"typecheck": "tsc --noEmit"
|
|
30
50
|
},
|
|
31
51
|
"dependencies": {
|
|
32
|
-
"@ottocode/sdk": "0.1.
|
|
33
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.197",
|
|
53
|
+
"@ottocode/database": "0.1.197",
|
|
34
54
|
"drizzle-orm": "^0.44.5",
|
|
35
55
|
"hono": "^4.9.9",
|
|
36
56
|
"zod": "^4.1.8"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type OauthCodexContinuationInput = {
|
|
2
|
+
provider: string;
|
|
3
|
+
isOpenAIOAuth: boolean;
|
|
4
|
+
finishObserved: boolean;
|
|
5
|
+
continuationCount: number;
|
|
6
|
+
maxContinuations: number;
|
|
7
|
+
finishReason?: string;
|
|
8
|
+
rawFinishReason?: string;
|
|
9
|
+
firstToolSeen: boolean;
|
|
10
|
+
droppedPseudoToolText: boolean;
|
|
11
|
+
lastAssistantText: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type OauthCodexContinuationDecision = {
|
|
15
|
+
shouldContinue: boolean;
|
|
16
|
+
reason?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const INTERMEDIATE_PROGRESS_PATTERNS: RegExp[] = [
|
|
20
|
+
/\bnext\s+i(?:['\u2019]ll|\s+will)\b/i,
|
|
21
|
+
/\bnow\s+i(?:['\u2019]ll|\s+will)\b/i,
|
|
22
|
+
/\bi(?:['\u2019]ll|\s+will)\s+(inspect|check|look|read|scan|trace|review|update|fix|implement|run|continue|retry)\b/i,
|
|
23
|
+
/\bi(?:\s+am|\s*'m)\s+going\s+to\b/i,
|
|
24
|
+
/\b(and|then)\s+continue\b/i,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detects whether assistant text looks like an intermediate progress update
|
|
29
|
+
* (e.g. "Next I'll inspect...") rather than a final user-facing completion.
|
|
30
|
+
*/
|
|
31
|
+
export function looksLikeIntermediateProgressText(text: string): boolean {
|
|
32
|
+
const trimmed = text.trim();
|
|
33
|
+
if (!trimmed) return false;
|
|
34
|
+
return INTERMEDIATE_PROGRESS_PATTERNS.some((pattern) =>
|
|
35
|
+
pattern.test(trimmed),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isTruncatedResponse(
|
|
40
|
+
finishReason?: string,
|
|
41
|
+
rawFinishReason?: string,
|
|
42
|
+
): boolean {
|
|
43
|
+
if (finishReason === 'length') return true;
|
|
44
|
+
return rawFinishReason === 'max_output_tokens';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decides whether an OpenAI OAuth Codex turn should auto-continue to recover
|
|
49
|
+
* only from hard truncation. Other completion behavior is handled by
|
|
50
|
+
* stream step limits and prompt alignment, not synthetic continuation turns.
|
|
51
|
+
*/
|
|
52
|
+
export function decideOauthCodexContinuation(
|
|
53
|
+
input: OauthCodexContinuationInput,
|
|
54
|
+
): OauthCodexContinuationDecision {
|
|
55
|
+
if (input.provider !== 'openai' || !input.isOpenAIOAuth) {
|
|
56
|
+
return { shouldContinue: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (input.finishObserved) {
|
|
60
|
+
return { shouldContinue: false };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (input.continuationCount >= input.maxContinuations) {
|
|
64
|
+
return { shouldContinue: false, reason: 'max-continuations-reached' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (isTruncatedResponse(input.finishReason, input.rawFinishReason)) {
|
|
68
|
+
return { shouldContinue: true, reason: 'truncated' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { shouldContinue: false };
|
|
72
|
+
}
|
|
@@ -110,6 +110,7 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
110
110
|
includeProjectTree: isFirstMessage,
|
|
111
111
|
userContext: opts.userContext,
|
|
112
112
|
contextSummary,
|
|
113
|
+
isOpenAIOAuth: oauth.isOpenAIOAuth,
|
|
113
114
|
});
|
|
114
115
|
|
|
115
116
|
const rawMaxOutputTokens = getMaxOutputTokens(opts.provider, opts.model);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasToolCall, streamText } from 'ai';
|
|
1
|
+
import { hasToolCall, stepCountIs, streamText } from 'ai';
|
|
2
2
|
import { messages, messageParts } from '@ottocode/database/schema';
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
4
|
import { publish, subscribe } from '../../events/bus.ts';
|
|
@@ -32,6 +32,11 @@ import {
|
|
|
32
32
|
handleReasoningDelta,
|
|
33
33
|
handleReasoningEnd,
|
|
34
34
|
} from './runner-reasoning.ts';
|
|
35
|
+
import {
|
|
36
|
+
createOauthCodexTextGuardState,
|
|
37
|
+
consumeOauthCodexTextDelta,
|
|
38
|
+
} from '../stream/text-guard.ts';
|
|
39
|
+
import { decideOauthCodexContinuation } from './oauth-codex-continuation.ts';
|
|
35
40
|
|
|
36
41
|
export {
|
|
37
42
|
enqueueAssistantRun,
|
|
@@ -94,11 +99,34 @@ async function runAssistant(opts: RunOpts) {
|
|
|
94
99
|
}> = [...additionalSystemMessages, ...history];
|
|
95
100
|
|
|
96
101
|
if (!isFirstMessage) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
if (isOpenAIOAuth) {
|
|
103
|
+
messagesWithSystemInstructions.push({
|
|
104
|
+
role: 'system',
|
|
105
|
+
content:
|
|
106
|
+
'SYSTEM REMINDER: You are continuing an existing session. Continue executing directly, use tools as needed, and provide a concise final summary when complete.',
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
messagesWithSystemInstructions.push({
|
|
110
|
+
role: 'user',
|
|
111
|
+
content:
|
|
112
|
+
'SYSTEM REMINDER: You are continuing an existing session. When you have completed the task, you MUST stream a text summary of what you did to the user, and THEN call the `finish` tool. Do not call `finish` without a summary.',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if ((opts.continuationCount ?? 0) > 0) {
|
|
117
|
+
if (isOpenAIOAuth) {
|
|
118
|
+
messagesWithSystemInstructions.push({
|
|
119
|
+
role: 'system',
|
|
120
|
+
content:
|
|
121
|
+
'SYSTEM REMINDER: Your previous response stopped mid-task. Continue immediately from where you left off and finish the actual implementation, not just a plan update.',
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
messagesWithSystemInstructions.push({
|
|
125
|
+
role: 'user',
|
|
126
|
+
content:
|
|
127
|
+
'SYSTEM REMINDER: Your previous response stopped before calling `finish`. Continue executing immediately from where you left off, avoid plan-only updates, and call `finish` only after streaming the final user summary.',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
debugLog(
|
|
@@ -123,7 +151,11 @@ async function runAssistant(opts: RunOpts) {
|
|
|
123
151
|
|
|
124
152
|
let currentPartId: string | null = null;
|
|
125
153
|
let accumulated = '';
|
|
154
|
+
let latestAssistantText = '';
|
|
126
155
|
let stepIndex = 0;
|
|
156
|
+
const oauthTextGuard = isOpenAIOAuth
|
|
157
|
+
? createOauthCodexTextGuardState()
|
|
158
|
+
: null;
|
|
127
159
|
|
|
128
160
|
const getCurrentPartId = () => currentPartId;
|
|
129
161
|
const getStepIndex = () => stepIndex;
|
|
@@ -164,6 +196,9 @@ async function runAssistant(opts: RunOpts) {
|
|
|
164
196
|
const onAbort = createAbortHandler(opts, db, getStepIndex, sharedCtx);
|
|
165
197
|
|
|
166
198
|
const onFinish = createFinishHandler(opts, db, completeAssistantMessage);
|
|
199
|
+
const stopWhenCondition = isOpenAIOAuth
|
|
200
|
+
? stepCountIs(48)
|
|
201
|
+
: hasToolCall('finish');
|
|
167
202
|
|
|
168
203
|
try {
|
|
169
204
|
const result = streamText({
|
|
@@ -177,7 +212,7 @@ async function runAssistant(opts: RunOpts) {
|
|
|
177
212
|
: {}),
|
|
178
213
|
...(Object.keys(providerOptions).length > 0 ? { providerOptions } : {}),
|
|
179
214
|
abortSignal: opts.abortSignal,
|
|
180
|
-
stopWhen:
|
|
215
|
+
stopWhen: stopWhenCondition,
|
|
181
216
|
// biome-ignore lint/suspicious/noExplicitAny: AI SDK callback types mismatch
|
|
182
217
|
onStepFinish: onStepFinish as any,
|
|
183
218
|
// biome-ignore lint/suspicious/noExplicitAny: AI SDK callback types mismatch
|
|
@@ -193,10 +228,18 @@ async function runAssistant(opts: RunOpts) {
|
|
|
193
228
|
if (!part) continue;
|
|
194
229
|
|
|
195
230
|
if (part.type === 'text-delta') {
|
|
196
|
-
const
|
|
231
|
+
const rawDelta = part.text;
|
|
232
|
+
if (!rawDelta) continue;
|
|
233
|
+
|
|
234
|
+
const delta = oauthTextGuard
|
|
235
|
+
? consumeOauthCodexTextDelta(oauthTextGuard, rawDelta)
|
|
236
|
+
: rawDelta;
|
|
197
237
|
if (!delta) continue;
|
|
198
238
|
|
|
199
239
|
accumulated += delta;
|
|
240
|
+
if (accumulated.trim()) {
|
|
241
|
+
latestAssistantText = accumulated;
|
|
242
|
+
}
|
|
200
243
|
|
|
201
244
|
if (!currentPartId && !accumulated.trim()) {
|
|
202
245
|
continue;
|
|
@@ -282,6 +325,11 @@ async function runAssistant(opts: RunOpts) {
|
|
|
282
325
|
}
|
|
283
326
|
|
|
284
327
|
const fs = firstToolSeen();
|
|
328
|
+
if (oauthTextGuard?.dropped) {
|
|
329
|
+
debugLog(
|
|
330
|
+
'[RUNNER] Dropped pseudo tool-call text leaked by OpenAI OAuth stream',
|
|
331
|
+
);
|
|
332
|
+
}
|
|
285
333
|
if (!fs && !_finishObserved) {
|
|
286
334
|
publish({
|
|
287
335
|
type: 'finish-step',
|
|
@@ -301,66 +349,81 @@ async function runAssistant(opts: RunOpts) {
|
|
|
301
349
|
streamFinishReason = undefined;
|
|
302
350
|
}
|
|
303
351
|
|
|
352
|
+
let streamRawFinishReason: string | undefined;
|
|
353
|
+
try {
|
|
354
|
+
streamRawFinishReason = await result.rawFinishReason;
|
|
355
|
+
} catch {
|
|
356
|
+
streamRawFinishReason = undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
304
359
|
debugLog(
|
|
305
|
-
`[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}, finishReason=${streamFinishReason}`,
|
|
360
|
+
`[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}`,
|
|
306
361
|
);
|
|
307
362
|
|
|
308
|
-
const
|
|
363
|
+
const MAX_CONTINUATIONS = 6;
|
|
364
|
+
const continuationCount = opts.continuationCount ?? 0;
|
|
365
|
+
const continuationDecision = decideOauthCodexContinuation({
|
|
366
|
+
provider: opts.provider,
|
|
367
|
+
isOpenAIOAuth,
|
|
368
|
+
finishObserved: _finishObserved,
|
|
369
|
+
continuationCount,
|
|
370
|
+
maxContinuations: MAX_CONTINUATIONS,
|
|
371
|
+
finishReason: streamFinishReason,
|
|
372
|
+
rawFinishReason: streamRawFinishReason,
|
|
373
|
+
firstToolSeen: fs,
|
|
374
|
+
droppedPseudoToolText: oauthTextGuard?.dropped ?? false,
|
|
375
|
+
lastAssistantText: latestAssistantText,
|
|
376
|
+
});
|
|
309
377
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
(wasTruncated || fs);
|
|
378
|
+
if (continuationDecision.shouldContinue) {
|
|
379
|
+
debugLog(
|
|
380
|
+
`[RUNNER] WARNING: Stream ended without finish. reason=${continuationDecision.reason ?? 'unknown'}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}, firstToolSeen=${fs}. Auto-continuing.`,
|
|
381
|
+
);
|
|
315
382
|
|
|
316
|
-
if (shouldContinue) {
|
|
317
383
|
debugLog(
|
|
318
|
-
`[RUNNER]
|
|
384
|
+
`[RUNNER] Auto-continuing (${continuationCount + 1}/${MAX_CONTINUATIONS})...`,
|
|
319
385
|
);
|
|
320
386
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
387
|
+
try {
|
|
388
|
+
await completeAssistantMessage({}, opts, db);
|
|
389
|
+
} catch (err) {
|
|
324
390
|
debugLog(
|
|
325
|
-
`[RUNNER]
|
|
391
|
+
`[RUNNER] completeAssistantMessage failed before continuation: ${err instanceof Error ? err.message : String(err)}`,
|
|
326
392
|
);
|
|
393
|
+
}
|
|
327
394
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
sessionId: opts.sessionId,
|
|
340
|
-
role: 'assistant',
|
|
341
|
-
status: 'pending',
|
|
342
|
-
agent: opts.agent,
|
|
343
|
-
provider: opts.provider,
|
|
344
|
-
model: opts.model,
|
|
345
|
-
createdAt: Date.now(),
|
|
346
|
-
});
|
|
395
|
+
const continuationMessageId = crypto.randomUUID();
|
|
396
|
+
await db.insert(messages).values({
|
|
397
|
+
id: continuationMessageId,
|
|
398
|
+
sessionId: opts.sessionId,
|
|
399
|
+
role: 'assistant',
|
|
400
|
+
status: 'pending',
|
|
401
|
+
agent: opts.agent,
|
|
402
|
+
provider: opts.provider,
|
|
403
|
+
model: opts.model,
|
|
404
|
+
createdAt: Date.now(),
|
|
405
|
+
});
|
|
347
406
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
407
|
+
publish({
|
|
408
|
+
type: 'message.created',
|
|
409
|
+
sessionId: opts.sessionId,
|
|
410
|
+
payload: { id: continuationMessageId, role: 'assistant' },
|
|
411
|
+
});
|
|
353
412
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
413
|
+
enqueueAssistantRun(
|
|
414
|
+
{
|
|
415
|
+
...opts,
|
|
416
|
+
assistantMessageId: continuationMessageId,
|
|
417
|
+
continuationCount: continuationCount + 1,
|
|
418
|
+
},
|
|
419
|
+
runSessionLoop,
|
|
420
|
+
);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (
|
|
424
|
+
continuationDecision.reason === 'max-continuations-reached' &&
|
|
425
|
+
!_finishObserved
|
|
426
|
+
) {
|
|
364
427
|
debugLog(
|
|
365
428
|
`[RUNNER] Max continuations (${MAX_CONTINUATIONS}) reached, stopping.`,
|
|
366
429
|
);
|
|
@@ -23,7 +23,11 @@ export async function buildHistoryMessages(
|
|
|
23
23
|
const toolHistory = new ToolHistoryTracker();
|
|
24
24
|
|
|
25
25
|
for (const m of rows) {
|
|
26
|
-
if (
|
|
26
|
+
if (
|
|
27
|
+
m.role === 'assistant' &&
|
|
28
|
+
m.status !== 'complete' &&
|
|
29
|
+
m.status !== 'completed'
|
|
30
|
+
) {
|
|
27
31
|
debugLog(
|
|
28
32
|
`[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status} (current turn still in progress)`,
|
|
29
33
|
);
|
|
@@ -12,6 +12,10 @@ import GUIDED_PROMPT from '@ottocode/sdk/prompts/modes/guided.txt' with {
|
|
|
12
12
|
type: 'text',
|
|
13
13
|
};
|
|
14
14
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
15
|
+
import OPENAI_OAUTH_PROMPT from '@ottocode/sdk/prompts/providers/openai-oauth.txt' with {
|
|
16
|
+
type: 'text',
|
|
17
|
+
};
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
15
19
|
import ANTHROPIC_SPOOF_PROMPT from '@ottocode/sdk/prompts/providers/anthropicSpoof.txt' with {
|
|
16
20
|
type: 'text',
|
|
17
21
|
};
|
|
@@ -35,6 +39,7 @@ export async function composeSystemPrompt(options: {
|
|
|
35
39
|
includeProjectTree?: boolean;
|
|
36
40
|
userContext?: string;
|
|
37
41
|
contextSummary?: string;
|
|
42
|
+
isOpenAIOAuth?: boolean;
|
|
38
43
|
}): Promise<ComposedSystemPrompt> {
|
|
39
44
|
const components: string[] = [];
|
|
40
45
|
if (options.spoofPrompt) {
|
|
@@ -49,27 +54,38 @@ export async function composeSystemPrompt(options: {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
const parts: string[] = [];
|
|
57
|
+
if (options.isOpenAIOAuth) {
|
|
58
|
+
const oauthInstructions = (OPENAI_OAUTH_PROMPT || '').trim();
|
|
59
|
+
if (oauthInstructions) {
|
|
60
|
+
parts.push(oauthInstructions);
|
|
61
|
+
components.push('provider:openai-oauth');
|
|
62
|
+
}
|
|
63
|
+
if (options.agentPrompt.trim()) {
|
|
64
|
+
parts.push(options.agentPrompt.trim());
|
|
65
|
+
components.push('agent');
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const providerResult = await providerBasePrompt(
|
|
69
|
+
options.provider,
|
|
70
|
+
options.model,
|
|
71
|
+
options.projectRoot,
|
|
72
|
+
);
|
|
73
|
+
const baseInstructions = (BASE_PROMPT || '').trim();
|
|
52
74
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
if (baseInstructions.trim()) {
|
|
69
|
-
components.push('base');
|
|
70
|
-
}
|
|
71
|
-
if (options.agentPrompt.trim()) {
|
|
72
|
-
components.push('agent');
|
|
75
|
+
parts.push(
|
|
76
|
+
providerResult.prompt.trim(),
|
|
77
|
+
baseInstructions.trim(),
|
|
78
|
+
options.agentPrompt.trim(),
|
|
79
|
+
);
|
|
80
|
+
if (providerResult.prompt.trim()) {
|
|
81
|
+
components.push(`provider:${providerResult.resolvedType}`);
|
|
82
|
+
}
|
|
83
|
+
if (baseInstructions.trim()) {
|
|
84
|
+
components.push('base');
|
|
85
|
+
}
|
|
86
|
+
if (options.agentPrompt.trim()) {
|
|
87
|
+
components.push('agent');
|
|
88
|
+
}
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
if (options.oneShot) {
|
|
@@ -188,7 +188,7 @@ export function createErrorHandler(
|
|
|
188
188
|
} else {
|
|
189
189
|
await db
|
|
190
190
|
.update(messages)
|
|
191
|
-
.set({ status: '
|
|
191
|
+
.set({ status: 'complete', completedAt: Date.now() })
|
|
192
192
|
.where(eq(messages.id, opts.assistantMessageId));
|
|
193
193
|
|
|
194
194
|
publish({
|
|
@@ -260,7 +260,7 @@ export function createErrorHandler(
|
|
|
260
260
|
await db
|
|
261
261
|
.update(messages)
|
|
262
262
|
.set({
|
|
263
|
-
status: compactionSucceeded ? '
|
|
263
|
+
status: compactionSucceeded ? 'complete' : 'error',
|
|
264
264
|
completedAt: Date.now(),
|
|
265
265
|
})
|
|
266
266
|
.where(eq(messages.id, compactMessageId));
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type OauthCodexTextGuardState = {
|
|
2
|
+
raw: string;
|
|
3
|
+
sanitized: string;
|
|
4
|
+
dropped: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const LEAK_PATTERNS: RegExp[] = [
|
|
8
|
+
/assistant\s+to=/i,
|
|
9
|
+
/assistant\s+to\b/i,
|
|
10
|
+
/\bassistant\b\s*$/i,
|
|
11
|
+
/assistant\s+to=functions\./i,
|
|
12
|
+
/assistant\s+to=functions\b/i,
|
|
13
|
+
/to=functions\.[a-z0-9_]+\s+(commentary|analysis|final)\b/i,
|
|
14
|
+
/call:tool\{/i,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function findFirstLeakIndex(text: string): number {
|
|
18
|
+
let index = -1;
|
|
19
|
+
for (const pattern of LEAK_PATTERNS) {
|
|
20
|
+
const match = pattern.exec(text);
|
|
21
|
+
if (!match || match.index < 0) continue;
|
|
22
|
+
if (index === -1 || match.index < index) {
|
|
23
|
+
index = match.index;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return index;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Removes codex pseudo tool-call leakage from text streams.
|
|
31
|
+
*
|
|
32
|
+
* Some OAuth Codex responses leak harness syntax (e.g. "assistant to=functions...")
|
|
33
|
+
* into user-facing text. Once such a marker appears, everything from that marker
|
|
34
|
+
* onward is considered non-user text and dropped.
|
|
35
|
+
*/
|
|
36
|
+
export function stripCodexPseudoToolText(raw: string): {
|
|
37
|
+
sanitized: string;
|
|
38
|
+
dropped: boolean;
|
|
39
|
+
} {
|
|
40
|
+
const leakIndex = findFirstLeakIndex(raw);
|
|
41
|
+
if (leakIndex === -1) {
|
|
42
|
+
return { sanitized: raw, dropped: false };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
sanitized: raw.slice(0, leakIndex).trimEnd(),
|
|
46
|
+
dropped: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createOauthCodexTextGuardState(): OauthCodexTextGuardState {
|
|
51
|
+
return {
|
|
52
|
+
raw: '',
|
|
53
|
+
sanitized: '',
|
|
54
|
+
dropped: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Consumes a raw delta and returns only safe delta text.
|
|
60
|
+
*/
|
|
61
|
+
export function consumeOauthCodexTextDelta(
|
|
62
|
+
state: OauthCodexTextGuardState,
|
|
63
|
+
rawDelta: string,
|
|
64
|
+
): string {
|
|
65
|
+
if (!rawDelta) return '';
|
|
66
|
+
state.raw += rawDelta;
|
|
67
|
+
const next = stripCodexPseudoToolText(state.raw);
|
|
68
|
+
if (next.dropped) state.dropped = true;
|
|
69
|
+
|
|
70
|
+
let safeDelta = '';
|
|
71
|
+
if (next.sanitized.startsWith(state.sanitized)) {
|
|
72
|
+
safeDelta = next.sanitized.slice(state.sanitized.length);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
state.sanitized = next.sanitized;
|
|
76
|
+
return safeDelta;
|
|
77
|
+
}
|