@chibitek/chibi 0.1.158 → 0.1.161
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/assets/{abnfDiagram-O67JEVCF-B8-8zzMS.js → abnfDiagram-O67JEVCF-Bb-Unz3V.js} +1 -1
- package/dist/assets/{arc-CSHQ9Dx3.js → arc-HBMH7B4K.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-BCHM9rXW.js → architectureDiagram-NJMV4G6O-CYwkb2PE.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-B-5uxPFN.js → blockDiagram-BEXU5L5S-DPYYNxBH.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-CuKNM_MR.js → c4Diagram-YGBWAQC7-DZ9hKDlA.js} +1 -1
- package/dist/assets/channel-Nx85fHZ4.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNF02GM4.js → chunk-2Q5K7J3B-OKrUlts6.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-uEWgBd4M.js → chunk-3FUC2YCW-Tuur0lqz.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-eSZuXctY.js → chunk-5VM5RSS4-BL8QOZeP.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-CslS8nRS.js → chunk-GTNCS2PH-DMi8-ixt.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-w71-MeKj.js → chunk-GWA4HPMP-Dd7yNJ0G.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Bjju7WGh.js → chunk-JWPE2WC7-g2dOoIMX.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-HxrroD4M.js → chunk-NETBCI7D-Cum7feOh.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-D7Rz470x.js → chunk-WEXAMYUT-BzCC1jc4.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BWKnAeyM.js → chunk-XXDRQBXY-Ck63jUMo.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CeBDRHmP.js → classDiagram-v2-NBCMYWYE-DFSuRVMZ.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BBbRKlvr.js → cose-bilkent-JH36ORCC-CegRj_kY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-W02cnJ0B.js → cynefin-OW5HDTMX-cZS_kt1A.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Cs47eaxd.js → cynefinDiagram-VND7K2PF-DfQa9Rht.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-8mG18hzT.js → dagre-6A5THRUB-DJ9qftJu.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DF4Qrw67.js → diagram-22UHCM2B-KXUGUahJ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-3BAVoed1.js → diagram-3UASUU5V-BudCYrCp.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CKO3pECE.js → diagram-ATOU4E4O-Du2O03NS.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CZhrGvur.js → diagram-CDSNMT55-DXybXGYf.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-C07l9pdQ.js → diagram-MLGK6HIB-DfrknXGE.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D3or4lkZ.js → diagram-MPIPVDR6-C29FmaJm.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DV2epXp_.js → ebnfDiagram-ZINNZB2B-BF9wS0Nz.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DlvEn4-1.js → elk-276RUBZZ-B2Fah1VF.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CB6X7Wt5.js → erDiagram-OPXOYQCR-Di3q4bpF.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-C3bqz4lC.js → flowDiagram-KWPJA3E3-D1UiqwbF.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-yo_Evhab.js → ganttDiagram-FUAMR5RP-BjZqMt7o.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Cq2oUlap.js → gitGraphDiagram-X574FWY7-DiZtdip7.js} +1 -1
- package/dist/assets/index-B2fJMh8N.css +1 -0
- package/dist/assets/{index-CMcr5k3Z.js → index-XFwW09vd.js} +246 -246
- package/dist/assets/{index-CG4AFPb4.js → index-dbQveHan.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-Y3ZrY7qL.js → infoDiagram-VRGFBTTK-BJUJCgio.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BpOtSfyi.js → ishikawaDiagram-OU5B5YK6-BX0XdHUx.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-UQM727le.js → journeyDiagram-ZHPQQLJL-Bg904IIx.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-C3s5oHpY.js → kanban-definition-PNTS6WVX-hkZMFMHZ.js} +1 -1
- package/dist/assets/{linear-hX6nFOQr.js → linear-Bh29RTbQ.js} +1 -1
- package/dist/assets/{mermaid.core-DZObGLN7.js → mermaid.core-aqB1jW_w.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Br-i2E3P.js → mindmap-definition-NLK3R4M7-D5GMV-Zd.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-D5SERB-N.js → pegDiagram-GJSIUBJH-7_wFtkFH.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BPNf4J3D.js → pieDiagram-5QR66LMP-DXtZBQ4f.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js → quadrantDiagram-O4NWA36T-C1v3ydTF.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-D6bo0abD.js → railroadDiagram-XR7U4H2S-CLmhiOXg.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP--1n5rkV7.js → requirementDiagram-PLB6GJNP-BKxI2rh_.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-Ct4tP2kK.js → sankeyDiagram-IPEJSGJF-gKK-Qs7b.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CCG7O5fR.js → sequenceDiagram-PO4LG4MO-BT94Uowp.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CJJJra9c.js → sizeCapture-INFHLROL-RDJL6sCM.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BMdRvVEX.js → stateDiagram-v2-GCMORJYK-DqHMzv-J.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-B1wnogip.js → swimlanes-2SLR337P-B-NN0ql6.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BO2gR34z.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-l-3yhXUP.js → timeline-definition-EJHVYXUP-C51S9tpN.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DtwtCOoJ.js → usecaseDiagram-POWQR4AR-DyfNMmhq.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-BZAEce1B.js → vennDiagram-UO4OBE2U-D924r33S.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Na_BCMMO.js → wardleyDiagram-VNRHLVJA-DXSfsjUr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BW9dlUfE.js → xychartDiagram-PMCCYNJV-mYX9tnBU.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +1 -1
- package/dist-server/index.js +3611 -3082
- package/dist-server/openmausbot.js +370 -162
- package/dist-server/pair-cli.js +370 -162
- package/dist-server/server/attachment-mimes.js +58 -0
- package/dist-server/server/attachments.js +28 -67
- package/dist-server/server/avatar-image.js +1 -0
- package/dist-server/server/avatar-subscription.js +24 -1
- package/dist-server/server/checkpoint-recovery.js +18 -0
- package/dist-server/server/drivers/acp/core.js +17 -17
- package/dist-server/server/drivers/claude-message.js +12 -0
- package/dist-server/server/drivers/claude.js +9 -12
- package/dist-server/server/drivers/codex-image.js +141 -0
- package/dist-server/server/drivers/codex.js +6 -9
- package/dist-server/server/drivers/openai-chat.js +13 -2
- package/dist-server/server/engine-failover.js +44 -4
- package/dist-server/server/engine-failure.js +2 -2
- package/dist-server/server/harness-import/history.js +5 -5
- package/dist-server/server/harness-import/test-fixtures.js +1 -1
- package/dist-server/server/index.js +40 -26
- package/dist-server/server/message-reactions.js +11 -0
- package/dist-server/server/model-evidence-context.js +18 -0
- package/dist-server/server/model-evidence.js +145 -0
- package/dist-server/shared/emoji-catalog.json +1 -0
- package/dist-server/shared/emoji.js +27 -0
- package/dist-server/shared/mascot-bodies.js +9 -1
- package/dist-server/shared/model-evidence.js +25 -0
- package/dist-server/vps-container-mcp.js +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-bPN03jIt.js +0 -1
- package/dist/assets/index-CZjRcUdn.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +0 -8
- package/dist-server/connector-proxy.js +0 -205
- package/dist-server/server/composio.js +0 -1051
- package/dist-server/server/connector-proxy.js +0 -249
- package/dist-server/server/mcp-public-registry.js +0 -325
- package/dist-server/shared/connector-availability.js +0 -12
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Accepted upload mimes and the extensions stored on disk for them. */
|
|
2
|
+
/** Mimes the endpoint accepts, mapped to the extension stored on disk.
|
|
3
|
+
* Sniffing is not attempted — a lie here only changes the filename. */
|
|
4
|
+
export const IMAGE_MIMES = {
|
|
5
|
+
"image/png": ".png",
|
|
6
|
+
"image/jpeg": ".jpg",
|
|
7
|
+
"image/gif": ".gif",
|
|
8
|
+
"image/webp": ".webp",
|
|
9
|
+
};
|
|
10
|
+
/** Useful document and audio formats accepted by the upload endpoint.
|
|
11
|
+
* Generic archives, binaries, HTML, SVG, and executable/script mimes stay
|
|
12
|
+
* out. Office/OpenDocument packages are allowed because they are documents,
|
|
13
|
+
* despite using ZIP internally. The claimed mime determines the extension;
|
|
14
|
+
* an attacker-controlled filename never does. */
|
|
15
|
+
export const FILE_MIMES = {
|
|
16
|
+
"audio/opus": ".opus",
|
|
17
|
+
"audio/ogg": ".ogg",
|
|
18
|
+
"audio/mpeg": ".mp3",
|
|
19
|
+
"audio/mp4": ".m4a",
|
|
20
|
+
"audio/x-m4a": ".m4a",
|
|
21
|
+
"audio/aac": ".aac",
|
|
22
|
+
"audio/wav": ".wav",
|
|
23
|
+
"audio/x-wav": ".wav",
|
|
24
|
+
"audio/wave": ".wav",
|
|
25
|
+
"audio/flac": ".flac",
|
|
26
|
+
"audio/x-flac": ".flac",
|
|
27
|
+
"audio/webm": ".webm",
|
|
28
|
+
"text/plain": ".txt",
|
|
29
|
+
"text/markdown": ".md",
|
|
30
|
+
"text/csv": ".csv",
|
|
31
|
+
"text/tab-separated-values": ".tsv",
|
|
32
|
+
"application/json": ".json",
|
|
33
|
+
"application/pdf": ".pdf",
|
|
34
|
+
"application/rtf": ".rtf",
|
|
35
|
+
"text/rtf": ".rtf",
|
|
36
|
+
"application/msword": ".doc",
|
|
37
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
38
|
+
"application/vnd.ms-excel": ".xls",
|
|
39
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
40
|
+
"application/vnd.ms-powerpoint": ".ppt",
|
|
41
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
|
42
|
+
"application/vnd.oasis.opendocument.text": ".odt",
|
|
43
|
+
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
|
|
44
|
+
"application/vnd.oasis.opendocument.presentation": ".odp",
|
|
45
|
+
};
|
|
46
|
+
export function extensionForMime(mime) {
|
|
47
|
+
if (!mime)
|
|
48
|
+
return null;
|
|
49
|
+
return IMAGE_MIMES[mime.split(";")[0].trim().toLowerCase()] ?? null;
|
|
50
|
+
}
|
|
51
|
+
export function normalizedMime(mime) {
|
|
52
|
+
const value = mime?.split(";")[0]?.trim().toLowerCase();
|
|
53
|
+
return value || null;
|
|
54
|
+
}
|
|
55
|
+
export function extensionForFileMime(mime) {
|
|
56
|
+
const value = normalizedMime(mime);
|
|
57
|
+
return value ? FILE_MIMES[value] ?? null : null;
|
|
58
|
+
}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// ~/.openmausbot/attachments so every CLI engine can open them by path —
|
|
3
3
|
// the app never ships image bytes through the prompt itself.
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
import { createReadStream, linkSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
5
|
+
import { createReadStream, linkSync, mkdirSync, readFileSync, readdirSync, statSync, statfsSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
6
6
|
import { link, open, unlink } from "node:fs/promises";
|
|
7
7
|
import { join, extname } from "node:path";
|
|
8
8
|
import { DATA_DIR } from "./config.js";
|
|
9
|
+
import { FILE_MIMES, IMAGE_MIMES, extensionForFileMime, extensionForMime, normalizedMime } from "./attachment-mimes.js";
|
|
10
|
+
export { extensionForFileMime, extensionForMime };
|
|
9
11
|
export const ATTACHMENTS_DIR = join(DATA_DIR, "attachments");
|
|
10
12
|
/** The spec's ceiling: a screenshot bigger than this is rejected before it
|
|
11
13
|
* is ever buffered, matching the composer's existing size discipline. */
|
|
@@ -14,9 +16,14 @@ export const IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
|
14
16
|
* ceiling. This is a local inbox, not unbounded remote storage. */
|
|
15
17
|
export const FILE_MAX_BYTES = 25 * 1024 * 1024;
|
|
16
18
|
/** Attachments are durable because prompts refer to their paths. Never
|
|
17
|
-
* silently evict them
|
|
18
|
-
*
|
|
19
|
-
|
|
19
|
+
* silently evict them. There is no fixed quota by default: the store grows
|
|
20
|
+
* until the disk itself runs low (see ATTACHMENTS_DISK_RESERVE_BYTES), then a
|
|
21
|
+
* new upload gets an explicit 507 and the person can decide what to remove.
|
|
22
|
+
* OMB_ATTACHMENTS_MAX_BYTES sets an optional hard ceiling (tests use it). */
|
|
23
|
+
export const ATTACHMENTS_MAX_BYTES = Number(process.env.OMB_ATTACHMENTS_MAX_BYTES) || Number.POSITIVE_INFINITY;
|
|
24
|
+
/** Free space always left on the attachments volume, so uploads can never
|
|
25
|
+
* fill the disk the app, its database and the OS depend on. */
|
|
26
|
+
export const ATTACHMENTS_DISK_RESERVE_BYTES = 2 * 1024 * 1024 * 1024;
|
|
20
27
|
/** Interrupted streamed uploads use private partial files. A crash can leave
|
|
21
28
|
* one behind, so future uploads remove only our stale partials — never a
|
|
22
29
|
* committed attachment that a transcript may still reference. */
|
|
@@ -47,55 +54,6 @@ function invalidateAttachmentAccounting() {
|
|
|
47
54
|
committedAttachmentBytes = null;
|
|
48
55
|
attachmentAccountingVersion += 1;
|
|
49
56
|
}
|
|
50
|
-
/** Mimes the endpoint accepts, mapped to the extension stored on disk.
|
|
51
|
-
* Sniffing is not attempted — a lie here only changes the filename. */
|
|
52
|
-
const IMAGE_MIMES = {
|
|
53
|
-
"image/png": ".png",
|
|
54
|
-
"image/jpeg": ".jpg",
|
|
55
|
-
"image/gif": ".gif",
|
|
56
|
-
"image/webp": ".webp",
|
|
57
|
-
};
|
|
58
|
-
/** Useful document and audio formats accepted by the upload endpoint.
|
|
59
|
-
* Generic archives, binaries, HTML, SVG, and executable/script mimes stay
|
|
60
|
-
* out. Office/OpenDocument packages are allowed because they are documents,
|
|
61
|
-
* despite using ZIP internally. The claimed mime determines the extension;
|
|
62
|
-
* an attacker-controlled filename never does. */
|
|
63
|
-
const FILE_MIMES = {
|
|
64
|
-
"audio/opus": ".opus",
|
|
65
|
-
"audio/ogg": ".ogg",
|
|
66
|
-
"audio/mpeg": ".mp3",
|
|
67
|
-
"audio/mp4": ".m4a",
|
|
68
|
-
"audio/x-m4a": ".m4a",
|
|
69
|
-
"audio/aac": ".aac",
|
|
70
|
-
"audio/wav": ".wav",
|
|
71
|
-
"audio/x-wav": ".wav",
|
|
72
|
-
"audio/wave": ".wav",
|
|
73
|
-
"audio/flac": ".flac",
|
|
74
|
-
"audio/x-flac": ".flac",
|
|
75
|
-
"audio/webm": ".webm",
|
|
76
|
-
"text/plain": ".txt",
|
|
77
|
-
"text/markdown": ".md",
|
|
78
|
-
"text/csv": ".csv",
|
|
79
|
-
"text/tab-separated-values": ".tsv",
|
|
80
|
-
"application/json": ".json",
|
|
81
|
-
"application/pdf": ".pdf",
|
|
82
|
-
"application/rtf": ".rtf",
|
|
83
|
-
"text/rtf": ".rtf",
|
|
84
|
-
"application/msword": ".doc",
|
|
85
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
86
|
-
"application/vnd.ms-excel": ".xls",
|
|
87
|
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
88
|
-
"application/vnd.ms-powerpoint": ".ppt",
|
|
89
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
|
90
|
-
"application/vnd.oasis.opendocument.text": ".odt",
|
|
91
|
-
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
|
|
92
|
-
"application/vnd.oasis.opendocument.presentation": ".odp",
|
|
93
|
-
};
|
|
94
|
-
export function extensionForMime(mime) {
|
|
95
|
-
if (!mime)
|
|
96
|
-
return null;
|
|
97
|
-
return IMAGE_MIMES[mime.split(";")[0].trim().toLowerCase()] ?? null;
|
|
98
|
-
}
|
|
99
57
|
export function ensureAttachmentsDir() {
|
|
100
58
|
mkdirSync(ATTACHMENTS_DIR, { recursive: true, mode: 0o700 });
|
|
101
59
|
}
|
|
@@ -160,11 +118,23 @@ function addCommittedBytes(delta, version = attachmentAccountingVersion) {
|
|
|
160
118
|
}
|
|
161
119
|
committedAttachmentBytes += delta;
|
|
162
120
|
}
|
|
163
|
-
/** Bytes that count against ATTACHMENTS_MAX_BYTES right now
|
|
164
|
-
|
|
165
|
-
export function attachmentBytesUsed() {
|
|
121
|
+
/** Bytes that count against ATTACHMENTS_MAX_BYTES right now. */
|
|
122
|
+
function attachmentBytesUsed() {
|
|
166
123
|
return committedBytes() + reservedAttachmentBytes;
|
|
167
124
|
}
|
|
125
|
+
/** Bytes the disk can still take before dropping to the reserve. Committed
|
|
126
|
+
* files are already off the free count; reservations are not written yet. */
|
|
127
|
+
function diskHeadroomBytes() {
|
|
128
|
+
ensureAttachmentsDir();
|
|
129
|
+
const { bavail, bsize } = statfsSync(ATTACHMENTS_DIR);
|
|
130
|
+
return Number(bavail) * Number(bsize) - ATTACHMENTS_DISK_RESERVE_BYTES - reservedAttachmentBytes;
|
|
131
|
+
}
|
|
132
|
+
/** How many more bytes uploads may add right now: the smaller of the
|
|
133
|
+
* optional ceiling and the disk headroom. Callers that plan a batch (the
|
|
134
|
+
* Grok Bot history import) read this before saving any of it. */
|
|
135
|
+
export function attachmentBytesAvailable() {
|
|
136
|
+
return Math.max(0, Math.min(ATTACHMENTS_MAX_BYTES - attachmentBytesUsed(), diskHeadroomBytes()));
|
|
137
|
+
}
|
|
168
138
|
/** Remove only abandoned temporary uploads, and refresh the cached usage
|
|
169
139
|
* total from the same fresh scan. Outside accounting recovery, this is the
|
|
170
140
|
* only full directory walk after start-up; nothing on the upload path calls it
|
|
@@ -239,10 +209,9 @@ class AttachmentReservation {
|
|
|
239
209
|
this.reserveAtLeast(bytes, bytes);
|
|
240
210
|
}
|
|
241
211
|
reserveAtLeast(required, preferred) {
|
|
242
|
-
const
|
|
243
|
-
const available = ATTACHMENTS_MAX_BYTES - used - reservedAttachmentBytes;
|
|
212
|
+
const available = attachmentBytesAvailable();
|
|
244
213
|
if (available < required) {
|
|
245
|
-
throw statusError(507,
|
|
214
|
+
throw statusError(507, "attachments storage is full (the disk is nearly out of free space)");
|
|
246
215
|
}
|
|
247
216
|
// Unknown-length HTTP bodies reserve in bounded chunks. Taking whatever
|
|
248
217
|
// remains below the preferred increment avoids falsely rejecting a small
|
|
@@ -328,14 +297,6 @@ async function digestFile(path) {
|
|
|
328
297
|
function statusError(status, message) {
|
|
329
298
|
return Object.assign(new Error(message), { status });
|
|
330
299
|
}
|
|
331
|
-
function normalizedMime(mime) {
|
|
332
|
-
const value = mime?.split(";")[0]?.trim().toLowerCase();
|
|
333
|
-
return value || null;
|
|
334
|
-
}
|
|
335
|
-
export function extensionForFileMime(mime) {
|
|
336
|
-
const value = normalizedMime(mime);
|
|
337
|
-
return value ? FILE_MIMES[value] ?? null : null;
|
|
338
|
-
}
|
|
339
300
|
/** Strip platform-reserved characters and replace any sender-provided
|
|
340
301
|
* extension with the canonical one for the accepted mime. Separators and
|
|
341
302
|
* encoded separators are rejected rather than massaged so traversal attempts
|
|
@@ -6,6 +6,7 @@ export const AVATAR_IMAGE_TIMEOUT_MS = 120_000;
|
|
|
6
6
|
const MAX_UPSTREAM_RESPONSE_BYTES = 15 * 1024 * 1024;
|
|
7
7
|
export const avatarGenerationRequestSchema = z.object({
|
|
8
8
|
prompt: z.string().trim().max(AVATAR_DIRECTION_MAX_CHARS).default(""),
|
|
9
|
+
source: z.enum(["auto", "subscription", "api"]).default("auto"),
|
|
9
10
|
});
|
|
10
11
|
const generatedImageResponseSchema = z.object({
|
|
11
12
|
data: z.array(z.object({ b64_json: z.string().min(1).optional(), url: z.string().optional() })).min(1),
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { decodeGeneratedImage } from "./generated-image.js";
|
|
2
|
-
import { avatarGenerationPrompt, AVATAR_IMAGE_TIMEOUT_MS } from "./avatar-image.js";
|
|
2
|
+
import { avatarGenerationPrompt, generateAvatarImage, AVATAR_IMAGE_TIMEOUT_MS } from "./avatar-image.js";
|
|
3
|
+
import { PASS, ROUTES } from "./routes/table.js";
|
|
4
|
+
export async function generateRoutedAvatar(cfg, instances, bot, direction, source = "auto") {
|
|
5
|
+
if (source !== "api") {
|
|
6
|
+
const subscription = await avatarSubscription(instances);
|
|
7
|
+
if (subscription)
|
|
8
|
+
return generateSubscriptionAvatar(subscription, bot, direction);
|
|
9
|
+
if (source === "subscription" || instances.some(instance => instance.enabled && instance.generateImage)) {
|
|
10
|
+
throw Object.assign(new Error("Sign in to an image-capable subscription in Settings, or explicitly choose an image API connection."), { status: 409 });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return generateAvatarImage(cfg, bot, direction);
|
|
14
|
+
}
|
|
15
|
+
export function installAvatarSubscriptionRoute(instances) {
|
|
16
|
+
ROUTES.push(async (context) => {
|
|
17
|
+
if (context.path !== "/api/avatar-subscription" || context.method !== "GET")
|
|
18
|
+
return PASS;
|
|
19
|
+
const available = instances();
|
|
20
|
+
const subscription = await avatarSubscription(available);
|
|
21
|
+
context.res.setHeader("Cache-Control", "no-store");
|
|
22
|
+
context.json(context.res, 200, { offered: available.some(instance => instance.enabled && instance.generateImage),
|
|
23
|
+
available: Boolean(subscription), label: subscription?.displayName ?? "Signed-in subscription" });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
3
26
|
/** Capability and current billing are both required. An authenticated text
|
|
4
27
|
* engine is not evidence that its subscription can generate images. */
|
|
5
28
|
export async function avatarSubscription(instances) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** This identifies an unusable saved session, never permission to replay a
|
|
2
|
+
* submitted prompt. Only a subsequent explicit turn may rebuild the context. */
|
|
3
|
+
export function isBrokenAntigravityCheckpoint(driverKind, message) {
|
|
4
|
+
return driverKind === "antigravityAgent" && message.includes("could not find doneCh for checkpoint");
|
|
5
|
+
}
|
|
6
|
+
export function invalidateBrokenCheckpoint(store, botId, driverKind, event) {
|
|
7
|
+
if (event.providerInstanceId && isBrokenAntigravityCheckpoint(driverKind, event.message)) {
|
|
8
|
+
// Undefined is absent on disk and engineIsFresh rebuilds from Atomic's
|
|
9
|
+
// transcript on the next send, including after an application restart.
|
|
10
|
+
store.setResumeCursor(botId, event.providerInstanceId, undefined, event.threadId);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Antigravity also sends its execution failure as a chunked assistant reply
|
|
14
|
+
* followed by end_turn. A quoted error inside a normal answer is not failure. */
|
|
15
|
+
export function isAntigravityCheckpointReply(driverKind, text) {
|
|
16
|
+
return driverKind === "antigravityAgent"
|
|
17
|
+
&& /^Agent execution error: could not find doneCh for checkpoint\s*$/.test(text.trim());
|
|
18
|
+
}
|
|
@@ -48,6 +48,7 @@ import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
|
|
|
48
48
|
import { extractMcpImages } from "../../mcp-tool-images.js";
|
|
49
49
|
import { mcpToolRefFromLabel } from "../../../shared/mcp-tool-ref.js";
|
|
50
50
|
import { redactSecretsInText } from "../../redact.js";
|
|
51
|
+
import { isBrokenAntigravityCheckpoint, isAntigravityCheckpointReply } from "../../checkpoint-recovery.js";
|
|
51
52
|
import { recoveryPromptFor } from "../../resume-recovery.js";
|
|
52
53
|
import { isModelRejection } from "../retry.js";
|
|
53
54
|
import { engineFailureFromCode, isCredentialDead, isOpaqueRequestRejection, logAuthFailure, parseResetAt, resetHintFromData } from "../../engine-failure.js";
|
|
@@ -464,14 +465,15 @@ export function createAcpDriver(support) {
|
|
|
464
465
|
// turn synchronously must find this session free
|
|
465
466
|
session.current = null;
|
|
466
467
|
active.delete(threadId);
|
|
468
|
+
if (isAntigravityCheckpointReply(DRIVER_KIND, current.state.text)) {
|
|
469
|
+
ok = false;
|
|
470
|
+
stopReason = "invalid_checkpoint";
|
|
471
|
+
emit({ ...base(threadId, current.turnId), type: "runtime.error", message: current.state.text.trim() });
|
|
472
|
+
closeSession(threadId, "invalid-checkpoint");
|
|
473
|
+
}
|
|
467
474
|
current.flushAssistantText();
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
// exactly that (a provider may cut a reasoning-only stream and
|
|
471
|
-
// still answer end_turn), and ok:true would end the thread quietly
|
|
472
|
-
// while the person's message went unanswered. Keep the completion,
|
|
473
|
-
// but report it as a failure so terminal chips, incidents and
|
|
474
|
-
// follow-ups see what happened.
|
|
475
|
+
// A successful end_turn without a reply or tool result is a lost
|
|
476
|
+
// turn. Report failure so terminal chips and follow-ups reflect it.
|
|
475
477
|
let finalOk = ok;
|
|
476
478
|
let finalStopReason = stopReason;
|
|
477
479
|
if (finalOk && finalStopReason === null && !current.state.producedItem) {
|
|
@@ -1467,7 +1469,9 @@ export function createAcpDriver(support) {
|
|
|
1467
1469
|
type: "runtime.error",
|
|
1468
1470
|
message: errorMessage,
|
|
1469
1471
|
});
|
|
1470
|
-
|
|
1472
|
+
if (isBrokenAntigravityCheckpoint(DRIVER_KIND, errorMessage))
|
|
1473
|
+
closeSession(threadId, "invalid-checkpoint");
|
|
1474
|
+
settle(threadId, session, false, isBrokenAntigravityCheckpoint(DRIVER_KIND, errorMessage) ? "invalid_checkpoint" : reason ?? "failed");
|
|
1471
1475
|
}
|
|
1472
1476
|
}
|
|
1473
1477
|
catch (e) {
|
|
@@ -1492,18 +1496,14 @@ export function createAcpDriver(support) {
|
|
|
1492
1496
|
...(needsAuth ? { setup: true } : {}),
|
|
1493
1497
|
...(failure ? { failure } : {}),
|
|
1494
1498
|
});
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1498
|
-
|
|
1499
|
-
// resume the recorded session on a fresh process.
|
|
1500
|
-
// An agent refused by its provider mid-prompt is still running
|
|
1501
|
-
// (retrying on its own): it goes too.
|
|
1502
|
-
if ((logAuth || (!needsAuth && (stalled || e?.acpSessionFailure === true
|
|
1499
|
+
// Evict unusable sessions without replaying accepted prompts.
|
|
1500
|
+
// The harness invalidates broken checkpoints durably so the
|
|
1501
|
+
// next explicit turn rebuilds from the retained transcript.
|
|
1502
|
+
if ((logAuth || (!needsAuth && (stalled || isBrokenAntigravityCheckpoint(DRIVER_KIND, message) || e?.acpSessionFailure === true
|
|
1503
1503
|
|| code === "upstream_outage"))) && session.child.exitCode === null && !session.closing) {
|
|
1504
1504
|
closeSession(threadId, logAuth ? "provider-auth" : stalled ? "prompt-stall" : "rpc-failure");
|
|
1505
1505
|
}
|
|
1506
|
-
settle(threadId, session, false, needsAuth ? "auth_required" : "rpc_error");
|
|
1506
|
+
settle(threadId, session, false, needsAuth ? "auth_required" : isBrokenAntigravityCheckpoint(DRIVER_KIND, message) ? "invalid_checkpoint" : "rpc_error");
|
|
1507
1507
|
}
|
|
1508
1508
|
}
|
|
1509
1509
|
})();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Extract text from the native Claude response before transcript normalization.
|
|
2
|
+
export function firstText(content) {
|
|
3
|
+
if (typeof content === "string")
|
|
4
|
+
return content;
|
|
5
|
+
if (Array.isArray(content)) {
|
|
6
|
+
return content
|
|
7
|
+
.filter((b) => b?.type === "text" && b.text)
|
|
8
|
+
.map((b) => b.text)
|
|
9
|
+
.join("");
|
|
10
|
+
}
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { firstText } from "./claude-message.js";
|
|
1
2
|
// Claude driver — upstream ClaudeDriver skeleton over agentcal's
|
|
2
3
|
// drivers/claude.js runtime (stream-json both directions, prompt over
|
|
3
4
|
// stdin, completion from a real `result` event — verified against
|
|
@@ -811,17 +812,6 @@ function decodeConfig(raw) {
|
|
|
811
812
|
...(disallowedTools !== undefined ? { disallowedTools } : {}),
|
|
812
813
|
};
|
|
813
814
|
}
|
|
814
|
-
function firstText(content) {
|
|
815
|
-
if (typeof content === "string")
|
|
816
|
-
return content;
|
|
817
|
-
if (Array.isArray(content)) {
|
|
818
|
-
return content
|
|
819
|
-
.filter((b) => b?.type === "text" && b.text)
|
|
820
|
-
.map((b) => b.text)
|
|
821
|
-
.join("");
|
|
822
|
-
}
|
|
823
|
-
return "";
|
|
824
|
-
}
|
|
825
815
|
/** Claude's stream-json input accepts the same image source blocks as the
|
|
826
816
|
* Anthropic Messages API. Keep the old string form for text-only turns so a
|
|
827
817
|
* CLI update cannot disturb the overwhelmingly common path. */
|
|
@@ -1739,7 +1729,7 @@ export const ClaudeDriver = {
|
|
|
1739
1729
|
delivered.sessionId = o.session_id;
|
|
1740
1730
|
session.sessionId = o.session_id;
|
|
1741
1731
|
}
|
|
1742
|
-
emit({ ...base(threadId, currentTurnId()), type: "session.started", sessionId: o.session_id, model: o.model, ...(retry.rebuilt ? { rebuilt: true } : {}) });
|
|
1732
|
+
emit({ ...base(threadId, currentTurnId()), type: "session.started", sessionId: o.session_id, model: o.model, modelEvidenceLevel: "session-reported", ...(retry.rebuilt ? { rebuilt: true } : {}) });
|
|
1743
1733
|
}
|
|
1744
1734
|
else if (o.subtype === "compact_boundary") {
|
|
1745
1735
|
// The CLI summarized the history, notes included: the next
|
|
@@ -1773,6 +1763,13 @@ export const ClaudeDriver = {
|
|
|
1773
1763
|
}
|
|
1774
1764
|
case "assistant": {
|
|
1775
1765
|
const msg = o.message ?? {};
|
|
1766
|
+
if (o.is_api_error_message !== true && typeof o.error !== "string") {
|
|
1767
|
+
emit({ ...base(threadId, currentTurnId()), type: "model.evidence",
|
|
1768
|
+
callId: typeof msg.id === "string" ? msg.id : newId(),
|
|
1769
|
+
responseId: typeof msg.id === "string" ? msg.id : undefined,
|
|
1770
|
+
model: typeof msg.model === "string" ? msg.model : null,
|
|
1771
|
+
level: typeof msg.model === "string" ? "response-reported" : "unknown" });
|
|
1772
|
+
}
|
|
1776
1773
|
const text = firstText(msg.content);
|
|
1777
1774
|
// CLI auth diagnostics enter the shared fallback path, never assistant text.
|
|
1778
1775
|
if (claudeAuthFailure(o, text)) {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { killCliTree, spawnCli } from "../procs.js";
|
|
5
|
+
import { codexConfigMcpServers, codexUserMcpOffArgs } from "./codex-mcp-names.js";
|
|
6
|
+
import { codexPersonalSetupOffArgs } from "./codex-personal.js";
|
|
7
|
+
import { codexToolSurfaceArgs } from "./codex-tool-surface.js";
|
|
8
|
+
/** A separate ephemeral app-server: no chat bus, history, mounted integrations,
|
|
9
|
+
* API billing fallback or provider-supplied file reads. Prompt travels on stdin. */
|
|
10
|
+
export async function codexImage(cli, env, prompt, signal, diagnostic, model) {
|
|
11
|
+
signal.throwIfAborted();
|
|
12
|
+
const cwd = await mkdtemp(join(tmpdir(), "atomic-avatar-"));
|
|
13
|
+
const args = ["app-server", ...codexToolSurfaceArgs(),
|
|
14
|
+
...codexUserMcpOffArgs(codexConfigMcpServers(env, cwd)), ...codexPersonalSetupOffArgs(env, cwd),
|
|
15
|
+
...["shell_tool", "unified_exec", "view_image", "apps", "multi_agent", "multi_agent_v2", "code_mode"]
|
|
16
|
+
.flatMap(name => ["-c", `features.${name}=false`]),
|
|
17
|
+
"-c", "features.image_generation=true", "-c", 'web_search="disabled"',
|
|
18
|
+
"-c", 'model_provider="openai"', "-c", 'forced_login_method="chatgpt"',
|
|
19
|
+
"-c", "project_doc_max_bytes=0",
|
|
20
|
+
];
|
|
21
|
+
let child;
|
|
22
|
+
try {
|
|
23
|
+
child = spawnCli(cli, args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
await rm(cwd, { recursive: true, force: true });
|
|
27
|
+
throw new Error("Could not start image generation.");
|
|
28
|
+
}
|
|
29
|
+
const pending = new Map();
|
|
30
|
+
let nextId = 0, buffer = "", image, done = false;
|
|
31
|
+
let resolveImage;
|
|
32
|
+
let rejectImage;
|
|
33
|
+
const result = new Promise((resolve, reject) => { resolveImage = resolve; rejectImage = reject; });
|
|
34
|
+
// The handshake may still be awaiting an RPC when an abort/exit occurs.
|
|
35
|
+
void result.catch(() => { });
|
|
36
|
+
const fail = () => {
|
|
37
|
+
const error = new Error("Codex image generation did not complete.");
|
|
38
|
+
for (const waiter of pending.values())
|
|
39
|
+
waiter.reject(error);
|
|
40
|
+
pending.clear();
|
|
41
|
+
if (!done)
|
|
42
|
+
rejectImage(error);
|
|
43
|
+
done = true;
|
|
44
|
+
};
|
|
45
|
+
const send = (message) => { child.stdin.write(JSON.stringify(message) + "\n", error => { if (error)
|
|
46
|
+
fail(); }); };
|
|
47
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
48
|
+
if (done) {
|
|
49
|
+
reject(new Error("Image session ended."));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const id = ++nextId;
|
|
53
|
+
pending.set(id, { resolve, reject });
|
|
54
|
+
send({ id, method, params });
|
|
55
|
+
});
|
|
56
|
+
child.on("error", fail);
|
|
57
|
+
child.on("exit", fail);
|
|
58
|
+
child.stdin.on("error", fail);
|
|
59
|
+
child.stderr.resume();
|
|
60
|
+
child.stdout.setEncoding("utf8");
|
|
61
|
+
child.stdout.on("data", (chunk) => {
|
|
62
|
+
buffer += chunk;
|
|
63
|
+
if (Buffer.byteLength(buffer) > 32 * 1024 * 1024) {
|
|
64
|
+
fail();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (;;) {
|
|
68
|
+
const newline = buffer.indexOf("\n");
|
|
69
|
+
if (newline < 0)
|
|
70
|
+
break;
|
|
71
|
+
const line = buffer.slice(0, newline);
|
|
72
|
+
buffer = buffer.slice(newline + 1);
|
|
73
|
+
try {
|
|
74
|
+
const message = JSON.parse(line);
|
|
75
|
+
if (message.method === "item/completed" || message.method === "turn/completed")
|
|
76
|
+
diagnostic?.({ method: message.method,
|
|
77
|
+
itemType: message.params?.item?.type, status: message.params?.turn?.status ?? message.params?.item?.status,
|
|
78
|
+
imageBytes: message.params?.item?.type === "imageGeneration" ? message.params.item.result?.length ?? 0 : undefined,
|
|
79
|
+
errorTerms: message.params?.turn?.error?.message?.match(/\b(?:usage|limit|quota|expired|auth|model|configuration|tools?|sandbox|rate|429|401|403|400|unsupported|invalid|image_generation|reasoning|summary|does not exist|not supported|not available|gpt-[\w.-]+)\b/gi) ?? undefined });
|
|
80
|
+
if (message.method && message.id !== undefined) {
|
|
81
|
+
// No permission, tool or user-input request can widen this session.
|
|
82
|
+
send({ id: message.id, error: { code: -32601, message: "Not available in avatar generation." } });
|
|
83
|
+
fail();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (message.id !== undefined) {
|
|
87
|
+
const waiter = pending.get(message.id);
|
|
88
|
+
pending.delete(message.id);
|
|
89
|
+
if (message.error)
|
|
90
|
+
waiter?.reject(new Error("Codex image request failed."));
|
|
91
|
+
else
|
|
92
|
+
waiter?.resolve(message.result);
|
|
93
|
+
}
|
|
94
|
+
else if (message.method === "item/completed" && message.params?.item?.type === "imageGeneration") {
|
|
95
|
+
const item = message.params.item;
|
|
96
|
+
if (item.status !== "failed" && typeof item.result === "string")
|
|
97
|
+
image = item.result;
|
|
98
|
+
}
|
|
99
|
+
else if (message.method === "turn/completed") {
|
|
100
|
+
if (message.params?.turn?.status !== "completed" || !image) {
|
|
101
|
+
fail();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
done = true;
|
|
105
|
+
resolveImage(image);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
fail();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
signal.addEventListener("abort", fail, { once: true });
|
|
115
|
+
try {
|
|
116
|
+
if (signal.aborted)
|
|
117
|
+
fail();
|
|
118
|
+
await request("initialize", { clientInfo: { name: "atomic_avatar", version: "1" }, capabilities: { experimentalApi: true } });
|
|
119
|
+
send({ method: "initialized", params: {} });
|
|
120
|
+
const catalog = await request("model/list", { limit: 100 });
|
|
121
|
+
const choices = Array.isArray(catalog.data) ? catalog.data.filter(item => item && item.hidden !== true &&
|
|
122
|
+
(typeof item.model === "string" || typeof item.id === "string")) : [];
|
|
123
|
+
const choice = choices.find(item => item.id === model || item.model === model) ?? choices.find(item => item.isDefault) ?? choices[0];
|
|
124
|
+
if (!choice)
|
|
125
|
+
throw new Error("No subscription image model is available.");
|
|
126
|
+
const started = await request("thread/start", { cwd, model: choice.model ?? choice.id, modelProvider: "openai", ephemeral: true, approvalPolicy: "never", sandbox: "read-only",
|
|
127
|
+
developerInstructions: "Generate exactly one image with the native image generation tool. Do not run commands, read files, use other tools or ask for credentials." });
|
|
128
|
+
if (typeof started.thread?.id !== "string")
|
|
129
|
+
throw new Error("Missing image session.");
|
|
130
|
+
await request("turn/start", { threadId: started.thread.id, input: [{ type: "text", text: prompt }],
|
|
131
|
+
approvalPolicy: "never", sandboxPolicy: { type: "readOnly" } });
|
|
132
|
+
return await result;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
signal.removeEventListener("abort", fail);
|
|
136
|
+
fail();
|
|
137
|
+
const stopped = await killCliTree(child);
|
|
138
|
+
if (stopped)
|
|
139
|
+
await rm(cwd, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { codexToolSurfaceArgs } from "./codex-tool-surface.js";
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// arrive as in-process server→client JSON-RPC requests and surface as
|
|
7
|
-
// canonical request.opened events (answered via respondToRequest — no MCP
|
|
8
|
-
// proxy or unix socket needed, unlike claude). Verified against
|
|
9
|
-
// codex-cli 0.144.4 by agentcal.
|
|
2
|
+
import { codexImage } from "./codex-image.js";
|
|
3
|
+
// Official Codex app-server over newline-delimited JSON-RPC on stdio.
|
|
4
|
+
// Completion comes from turn/completed; native approval requests become
|
|
5
|
+
// canonical request.opened events answered through respondToRequest.
|
|
10
6
|
//
|
|
11
7
|
// resumeCursor is the codex thread id; a later turn tries thread/resume
|
|
12
8
|
// and preserves that history or reports a failed resume.
|
|
@@ -1557,7 +1553,7 @@ export const CodexDriver = {
|
|
|
1557
1553
|
if (!codexThreadId)
|
|
1558
1554
|
throw new Error("Codex did not return a native thread id");
|
|
1559
1555
|
await syncCodexInstructions(threadId, codexThreadId, developerInstructions, resumedNativeThread, request);
|
|
1560
|
-
emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null, ...(rebuiltFromReplay ? { rebuilt: true } : {}) });
|
|
1556
|
+
emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null, modelEvidenceLevel: startedModel ? "session-reported" : "requested-only", ...(rebuiltFromReplay ? { rebuilt: true } : {}) });
|
|
1561
1557
|
const note = turn.systemStable !== undefined
|
|
1562
1558
|
? volatileNoteFor(threadId, codexThreadId, !resumedNativeThread || rebuiltFromReplay, turn)
|
|
1563
1559
|
: "";
|
|
@@ -1700,6 +1696,7 @@ export const CodexDriver = {
|
|
|
1700
1696
|
cancelAuthentication: () => authentication.cancel(),
|
|
1701
1697
|
signOut: () => authentication.signOut(),
|
|
1702
1698
|
snapshot,
|
|
1699
|
+
...(!config.managed ? { generateImage: (prompt, options) => codexImage(config.cli, childEnv(), prompt, options.signal, undefined, decodeCodexSelection(models.default).model ?? undefined) } : {}),
|
|
1703
1700
|
adapter: {
|
|
1704
1701
|
provider: DRIVER_KIND,
|
|
1705
1702
|
capabilities: {
|
|
@@ -42,7 +42,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
42
42
|
turnId,
|
|
43
43
|
createdAt: new Date().toISOString(),
|
|
44
44
|
});
|
|
45
|
-
const complete = async (messages, model, stream, signal, onDelta, tools = []) => {
|
|
45
|
+
const complete = async (messages, model, stream, signal, onDelta, tools = [], onIdentity) => {
|
|
46
46
|
// Idle timer that is renewed on every received chunk during streaming
|
|
47
47
|
const timeoutController = new AbortController();
|
|
48
48
|
let idleTimer = null;
|
|
@@ -77,6 +77,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
77
77
|
const bodyError = providerError(json);
|
|
78
78
|
if (bodyError)
|
|
79
79
|
throw new ChatProtocolError(`provider returned a completion error: ${bodyError.slice(0, 200)}`);
|
|
80
|
+
onIdentity?.(typeof json.model === "string" ? json.model : null, typeof json.id === "string" ? json.id : undefined);
|
|
80
81
|
const message = json.choices?.[0]?.message;
|
|
81
82
|
if (!message || !object(message) || !["content", "reasoning_content", "reasoning", "reasoning_details", "tool_calls", "function_call"].some((key) => key in message)) {
|
|
82
83
|
throw new ChatProtocolError("provider returned no completion message");
|
|
@@ -141,6 +142,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
141
142
|
const chunkError = providerError(chunk);
|
|
142
143
|
if (chunkError)
|
|
143
144
|
throw new ChatProtocolError(`provider returned a streaming completion error: ${chunkError.slice(0, 200)}`);
|
|
145
|
+
onIdentity?.(typeof chunk.model === "string" ? chunk.model : null, typeof chunk.id === "string" ? chunk.id : undefined);
|
|
144
146
|
const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
|
|
145
147
|
const delta = choice?.delta;
|
|
146
148
|
if (object(delta))
|
|
@@ -329,10 +331,19 @@ export function createOpenAIChatRuntime(options) {
|
|
|
329
331
|
emit({ ...base(turn.threadId, turnId), type: "content.delta", streamKind, delta: visible });
|
|
330
332
|
};
|
|
331
333
|
try {
|
|
334
|
+
const callId = newId();
|
|
335
|
+
emit({ ...base(turn.threadId, turnId), type: "model.call", callId });
|
|
336
|
+
let previousIdentity = "";
|
|
332
337
|
completion = await complete(messages, model, true, abort.signal, (text, streamKind) => {
|
|
333
338
|
streamed = true;
|
|
334
339
|
delta(text, streamKind);
|
|
335
|
-
}, tools.definitions)
|
|
340
|
+
}, tools.definitions, (reportedModel, responseId) => {
|
|
341
|
+
const identity = JSON.stringify([reportedModel, responseId]);
|
|
342
|
+
if (identity === previousIdentity)
|
|
343
|
+
return;
|
|
344
|
+
previousIdentity = identity;
|
|
345
|
+
emit({ ...base(turn.threadId, turnId), type: "model.evidence", callId, model: reportedModel, responseId, level: reportedModel ? "response-reported" : "unknown" });
|
|
346
|
+
});
|
|
336
347
|
delta("", "assistant_text", true);
|
|
337
348
|
delta("", "reasoning_text", true);
|
|
338
349
|
break;
|