pi-oracle 0.2.1 → 0.2.2
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 - 2026-04-07
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- missed ChatGPT file artifacts now map generic download controls onto nearby filenames and download from live DOM selectors instead of relying only on filename-labeled snapshot refs
|
|
7
|
+
- oracle jobs no longer report a false-clean completion when response-local artifact signals are present but capture fails or remains inconclusive
|
|
8
|
+
- artifact label extraction now collapses paths and mixed response text down to real filenames so suspicious artifact fallback logic does not emit bogus labels
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- regression coverage for artifact label extraction edge cases and ambiguous download-control artifact detection
|
|
12
|
+
|
|
3
13
|
## 0.2.1 - 2026-04-07
|
|
4
14
|
|
|
5
15
|
### Fixed
|
|
@@ -10,20 +10,36 @@ export interface SnapshotEntry {
|
|
|
10
10
|
|
|
11
11
|
export interface StructuralArtifactCandidateInput {
|
|
12
12
|
label?: string;
|
|
13
|
+
selector?: string;
|
|
14
|
+
controlLabel?: string;
|
|
13
15
|
paragraphText?: string;
|
|
14
16
|
listItemText?: string;
|
|
15
|
-
|
|
17
|
+
paragraphInteractiveCount?: number;
|
|
18
|
+
paragraphArtifactLabelCount?: number;
|
|
16
19
|
paragraphOtherTextLength?: number;
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
listItemInteractiveCount?: number;
|
|
21
|
+
listItemArtifactLabelCount?: number;
|
|
22
|
+
focusableInteractiveCount?: number;
|
|
23
|
+
focusableArtifactLabelCount?: number;
|
|
19
24
|
focusableOtherTextLength?: number;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export interface StructuralArtifactCandidate {
|
|
23
28
|
label: string;
|
|
29
|
+
selector?: string;
|
|
30
|
+
controlLabel?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StructuralArtifactCandidatePartition {
|
|
34
|
+
confirmed: StructuralArtifactCandidate[];
|
|
35
|
+
suspicious: StructuralArtifactCandidate[];
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
export function parseSnapshotEntries(snapshot: string): SnapshotEntry[];
|
|
39
|
+
export function extractArtifactLabels(value: string): string[];
|
|
27
40
|
export function filterStructuralArtifactCandidates(
|
|
28
41
|
candidates: StructuralArtifactCandidateInput[],
|
|
29
42
|
): StructuralArtifactCandidate[];
|
|
43
|
+
export function partitionStructuralArtifactCandidates(
|
|
44
|
+
candidates: StructuralArtifactCandidateInput[],
|
|
45
|
+
): StructuralArtifactCandidatePartition;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export const FILE_LABEL_PATTERN_SOURCE = String.raw`(?:^|[
|
|
2
|
-
const FILE_LABEL_PATTERN = new RegExp(FILE_LABEL_PATTERN_SOURCE);
|
|
1
|
+
export const FILE_LABEL_PATTERN_SOURCE = String.raw`(?:^|[^A-Za-z0-9._~/-])((?:(?:[A-Za-z]:)?[\\/]|[.~][\\/])?(?:[^\\/\s"'<>|]+[\\/])*[^\\/\s"'<>|]+\.[A-Za-z0-9]{1,12})(?=$|[^A-Za-z0-9._~/-])`;
|
|
2
|
+
const FILE_LABEL_PATTERN = new RegExp(FILE_LABEL_PATTERN_SOURCE, "g");
|
|
3
3
|
export const GENERIC_ARTIFACT_LABELS = ["ATTACHED", "DONE"];
|
|
4
4
|
const GENERIC_ARTIFACT_LABEL_SET = new Set(GENERIC_ARTIFACT_LABELS);
|
|
5
|
+
const GENERIC_DOWNLOAD_CONTROL_PATTERN = /(?:^|\b)(?:download|save)(?:\b|$)/i;
|
|
5
6
|
|
|
6
7
|
export function parseSnapshotEntries(snapshot) {
|
|
7
8
|
return String(snapshot || "")
|
|
@@ -29,11 +30,61 @@ function normalizeText(value) {
|
|
|
29
30
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function sanitizeArtifactLabel(value) {
|
|
34
|
+
const normalized = normalizeText(value).replace(/^[^A-Za-z0-9._~/-]+|[^A-Za-z0-9._~/-]+$/g, "");
|
|
35
|
+
if (!normalized) return "";
|
|
36
|
+
const basename = normalized.split(/[\\/]/).filter(Boolean).at(-1) || "";
|
|
37
|
+
return basename.replace(/^[^A-Za-z0-9._-]+|[^A-Za-z0-9._-]+$/g, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function extractArtifactLabels(value) {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const labels = [];
|
|
43
|
+
for (const match of String(value || "").matchAll(FILE_LABEL_PATTERN)) {
|
|
44
|
+
const normalized = sanitizeArtifactLabel(match[1] || match[0] || "");
|
|
45
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
46
|
+
seen.add(normalized);
|
|
47
|
+
labels.push(normalized);
|
|
48
|
+
}
|
|
49
|
+
return labels;
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
export function isLikelyArtifactLabel(label) {
|
|
33
53
|
const normalized = normalizeText(label);
|
|
34
54
|
if (!normalized) return false;
|
|
35
55
|
if (GENERIC_ARTIFACT_LABEL_SET.has(normalized.toUpperCase())) return true;
|
|
36
|
-
return
|
|
56
|
+
return extractArtifactLabels(normalized).length > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasGenericDownloadControl(controlLabel) {
|
|
60
|
+
return GENERIC_DOWNLOAD_CONTROL_PATTERN.test(normalizeText(controlLabel));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeCandidate(candidate) {
|
|
64
|
+
const label = normalizeText(candidate?.label);
|
|
65
|
+
return label ? { ...candidate, label } : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hasArtifactSignal(candidate) {
|
|
69
|
+
const label = normalizeText(candidate?.label);
|
|
70
|
+
if (!isLikelyArtifactLabel(label)) return false;
|
|
71
|
+
|
|
72
|
+
const paragraphInteractiveCount = Number(candidate?.paragraphInteractiveCount || 0);
|
|
73
|
+
const listItemInteractiveCount = Number(candidate?.listItemInteractiveCount || 0);
|
|
74
|
+
const focusableInteractiveCount = Number(candidate?.focusableInteractiveCount || 0);
|
|
75
|
+
const paragraphArtifactLabelCount = Number(candidate?.paragraphArtifactLabelCount || 0);
|
|
76
|
+
const listItemArtifactLabelCount = Number(candidate?.listItemArtifactLabelCount || 0);
|
|
77
|
+
const focusableArtifactLabelCount = Number(candidate?.focusableArtifactLabelCount || 0);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
hasGenericDownloadControl(candidate?.controlLabel) ||
|
|
81
|
+
paragraphInteractiveCount > 0 ||
|
|
82
|
+
listItemInteractiveCount > 0 ||
|
|
83
|
+
focusableInteractiveCount > 0 ||
|
|
84
|
+
paragraphArtifactLabelCount > 0 ||
|
|
85
|
+
listItemArtifactLabelCount > 0 ||
|
|
86
|
+
focusableArtifactLabelCount > 0
|
|
87
|
+
);
|
|
37
88
|
}
|
|
38
89
|
|
|
39
90
|
export function isStructuralArtifactCandidate(candidate) {
|
|
@@ -41,36 +92,56 @@ export function isStructuralArtifactCandidate(candidate) {
|
|
|
41
92
|
if (!isLikelyArtifactLabel(label)) return false;
|
|
42
93
|
|
|
43
94
|
const listItemText = normalizeText(candidate?.listItemText);
|
|
44
|
-
const
|
|
45
|
-
const
|
|
95
|
+
const listItemInteractiveCount = Number(candidate?.listItemInteractiveCount || 0);
|
|
96
|
+
const listItemArtifactLabelCount = Number(candidate?.listItemArtifactLabelCount || 0);
|
|
97
|
+
const paragraphInteractiveCount = Number(candidate?.paragraphInteractiveCount || 0);
|
|
98
|
+
const paragraphArtifactLabelCount = Number(candidate?.paragraphArtifactLabelCount || 0);
|
|
46
99
|
const paragraphOtherTextLength = Number(candidate?.paragraphOtherTextLength ?? Number.POSITIVE_INFINITY);
|
|
47
|
-
const
|
|
100
|
+
const focusableInteractiveCount = Number(candidate?.focusableInteractiveCount || 0);
|
|
101
|
+
const focusableArtifactLabelCount = Number(candidate?.focusableArtifactLabelCount || 0);
|
|
48
102
|
const focusableOtherTextLength = Number(candidate?.focusableOtherTextLength ?? Number.POSITIVE_INFINITY);
|
|
49
103
|
|
|
50
|
-
if (listItemText === label &&
|
|
104
|
+
if (listItemText === label && listItemInteractiveCount === 1 && listItemArtifactLabelCount === 1) {
|
|
51
105
|
return true;
|
|
52
106
|
}
|
|
53
107
|
|
|
54
|
-
if (
|
|
108
|
+
if (paragraphArtifactLabelCount === 1 && paragraphInteractiveCount === 1 && paragraphOtherTextLength <= 32) {
|
|
55
109
|
return true;
|
|
56
110
|
}
|
|
57
111
|
|
|
58
|
-
if (
|
|
112
|
+
if (focusableArtifactLabelCount >= 1 && focusableInteractiveCount >= 1 && focusableOtherTextLength <= 64) {
|
|
59
113
|
return true;
|
|
60
114
|
}
|
|
61
115
|
|
|
62
116
|
return false;
|
|
63
117
|
}
|
|
64
118
|
|
|
65
|
-
export function
|
|
66
|
-
const
|
|
67
|
-
const
|
|
119
|
+
export function partitionStructuralArtifactCandidates(candidates) {
|
|
120
|
+
const confirmedSeen = new Set();
|
|
121
|
+
const suspiciousSeen = new Set();
|
|
122
|
+
const confirmed = [];
|
|
123
|
+
const suspicious = [];
|
|
124
|
+
|
|
68
125
|
for (const candidate of candidates || []) {
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
if (!
|
|
72
|
-
|
|
73
|
-
|
|
126
|
+
const normalized = normalizeCandidate(candidate);
|
|
127
|
+
if (!normalized) continue;
|
|
128
|
+
if (!hasArtifactSignal(normalized)) continue;
|
|
129
|
+
|
|
130
|
+
if (isStructuralArtifactCandidate(normalized)) {
|
|
131
|
+
if (confirmedSeen.has(normalized.label)) continue;
|
|
132
|
+
confirmedSeen.add(normalized.label);
|
|
133
|
+
confirmed.push(normalized);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (suspiciousSeen.has(normalized.label)) continue;
|
|
138
|
+
suspiciousSeen.add(normalized.label);
|
|
139
|
+
suspicious.push(normalized);
|
|
74
140
|
}
|
|
75
|
-
|
|
141
|
+
|
|
142
|
+
return { confirmed, suspicious: suspicious.filter((candidate) => !confirmedSeen.has(candidate.label)) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function filterStructuralArtifactCandidates(candidates) {
|
|
146
|
+
return partitionStructuralArtifactCandidates(candidates).confirmed;
|
|
76
147
|
}
|
|
@@ -4,7 +4,7 @@ import { appendFile, chmod, mkdir, readFile, rename, rm, stat, writeFile } from
|
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { spawn, execFileSync } from "node:child_process";
|
|
7
|
-
import {
|
|
7
|
+
import { extractArtifactLabels, FILE_LABEL_PATTERN_SOURCE, GENERIC_ARTIFACT_LABELS, parseSnapshotEntries, partitionStructuralArtifactCandidates } from "./artifact-heuristics.mjs";
|
|
8
8
|
import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withLock } from "./state-locks.mjs";
|
|
9
9
|
|
|
10
10
|
const jobId = process.argv[2];
|
|
@@ -1497,28 +1497,52 @@ function preferredArtifactName(label, index) {
|
|
|
1497
1497
|
async function collectArtifactCandidates(job, responseIndex, responseText = "") {
|
|
1498
1498
|
const snapshot = await snapshotText(job);
|
|
1499
1499
|
const targetSlice = assistantSnapshotSlice(snapshot, responseIndex);
|
|
1500
|
-
if (!targetSlice) return { snapshot, targetSlice, candidates: [] };
|
|
1500
|
+
if (!targetSlice) return { snapshot, targetSlice, candidates: [], suspiciousLabels: [] };
|
|
1501
1501
|
|
|
1502
1502
|
const structural = await evalPage(
|
|
1503
1503
|
job,
|
|
1504
1504
|
toJsonScript(`
|
|
1505
1505
|
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1506
1506
|
const genericArtifactLabels = new Set(${JSON.stringify(GENERIC_ARTIFACT_LABELS)});
|
|
1507
|
-
const fileLabelPattern = new RegExp(${JSON.stringify(FILE_LABEL_PATTERN_SOURCE)});
|
|
1507
|
+
const fileLabelPattern = new RegExp(${JSON.stringify(FILE_LABEL_PATTERN_SOURCE)}, 'g');
|
|
1508
|
+
const downloadControlPattern = /(?:^|\\b)(?:download|save)(?:\\b|$)/i;
|
|
1509
|
+
const artifactMarkerAttr = 'data-pi-oracle-artifact-candidate';
|
|
1510
|
+
const artifactPrefix = 'pi-oracle-artifact-${jobId}-${responseIndex}-';
|
|
1511
|
+
const sanitize = (value) => normalize(value).replace(/^[^A-Za-z0-9._~/-]+|[^A-Za-z0-9._~/-]+$/g, '');
|
|
1512
|
+
const sanitizeArtifactLabel = (value) => {
|
|
1513
|
+
const normalized = sanitize(value);
|
|
1514
|
+
if (!normalized) return '';
|
|
1515
|
+
const basename = normalized.split(/[\\/]/).filter(Boolean).at(-1) || '';
|
|
1516
|
+
return basename.replace(/^[^A-Za-z0-9._-]+|[^A-Za-z0-9._-]+$/g, '');
|
|
1517
|
+
};
|
|
1518
|
+
const extractArtifactLabels = (value) => {
|
|
1519
|
+
const seen = new Set();
|
|
1520
|
+
const labels = [];
|
|
1521
|
+
for (const match of String(value || '').matchAll(fileLabelPattern)) {
|
|
1522
|
+
const label = sanitizeArtifactLabel(match[1] || match[0] || '');
|
|
1523
|
+
if (!label || seen.has(label)) continue;
|
|
1524
|
+
seen.add(label);
|
|
1525
|
+
labels.push(label);
|
|
1526
|
+
}
|
|
1527
|
+
return labels;
|
|
1528
|
+
};
|
|
1508
1529
|
const isFileLabel = (value) => {
|
|
1509
1530
|
const normalized = normalize(value);
|
|
1510
1531
|
if (!normalized) return false;
|
|
1511
1532
|
if (genericArtifactLabels.has(normalized.toUpperCase())) return true;
|
|
1512
|
-
return
|
|
1533
|
+
return extractArtifactLabels(normalized).length > 0;
|
|
1513
1534
|
};
|
|
1535
|
+
const isDownloadControl = (value) => downloadControlPattern.test(normalize(value));
|
|
1514
1536
|
const headings = Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]'))
|
|
1515
1537
|
.filter((el) => normalize(el.textContent) === 'ChatGPT said:');
|
|
1516
1538
|
const host = headings[${responseIndex}]?.nextElementSibling;
|
|
1517
1539
|
if (!host) return { candidates: [] };
|
|
1518
1540
|
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1541
|
+
const interactiveElements = (node) => node ? Array.from(node.querySelectorAll('button, a')) : [];
|
|
1542
|
+
const interactiveLabels = (node) => interactiveElements(node)
|
|
1543
|
+
.map((candidate) => normalize(candidate.textContent || candidate.getAttribute('aria-label') || candidate.getAttribute('title')))
|
|
1544
|
+
.filter(Boolean);
|
|
1545
|
+
const artifactLabelsForNode = (node) => extractArtifactLabels(node?.textContent || '');
|
|
1522
1546
|
const otherTextLength = (text, labels) => {
|
|
1523
1547
|
let remaining = normalize(text);
|
|
1524
1548
|
for (const label of labels || []) {
|
|
@@ -1528,25 +1552,43 @@ async function collectArtifactCandidates(job, responseIndex, responseText = "")
|
|
|
1528
1552
|
return remaining.length;
|
|
1529
1553
|
};
|
|
1530
1554
|
const focusableFor = (node) => node?.closest('[tabindex]');
|
|
1555
|
+
const uniqueLabel = (...groups) => {
|
|
1556
|
+
for (const group of groups) {
|
|
1557
|
+
const labels = Array.from(new Set((group || []).map(sanitizeArtifactLabel).filter(Boolean)));
|
|
1558
|
+
if (labels.length === 1) return labels[0];
|
|
1559
|
+
}
|
|
1560
|
+
return undefined;
|
|
1561
|
+
};
|
|
1531
1562
|
|
|
1532
|
-
const candidates =
|
|
1533
|
-
.map((button) => {
|
|
1534
|
-
const
|
|
1535
|
-
if (!isFileLabel(label)) return null;
|
|
1563
|
+
const candidates = interactiveElements(host)
|
|
1564
|
+
.map((button, index) => {
|
|
1565
|
+
const controlLabel = normalize(button.textContent || button.getAttribute('aria-label') || button.getAttribute('title'));
|
|
1536
1566
|
const paragraph = button.closest('p');
|
|
1537
1567
|
const listItem = button.closest('li');
|
|
1538
1568
|
const focusable = focusableFor(button);
|
|
1539
|
-
const
|
|
1540
|
-
const
|
|
1569
|
+
const ownArtifactLabels = extractArtifactLabels(controlLabel);
|
|
1570
|
+
const paragraphArtifactLabels = artifactLabelsForNode(paragraph);
|
|
1571
|
+
const listItemArtifactLabels = artifactLabelsForNode(listItem);
|
|
1572
|
+
const focusableArtifactLabels = artifactLabelsForNode(focusable);
|
|
1573
|
+
const label = uniqueLabel(ownArtifactLabels, listItemArtifactLabels, paragraphArtifactLabels, focusableArtifactLabels);
|
|
1574
|
+
if (!label && !isFileLabel(controlLabel) && !isDownloadControl(controlLabel)) return null;
|
|
1575
|
+
if (!label) return null;
|
|
1576
|
+
const marker = artifactPrefix + index;
|
|
1577
|
+
button.setAttribute(artifactMarkerAttr, marker);
|
|
1541
1578
|
return {
|
|
1542
1579
|
label,
|
|
1580
|
+
selector: '[' + artifactMarkerAttr + '="' + marker + '"]',
|
|
1581
|
+
controlLabel,
|
|
1543
1582
|
paragraphText: normalize(paragraph?.textContent),
|
|
1544
1583
|
listItemText: normalize(listItem?.textContent),
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1584
|
+
paragraphInteractiveCount: interactiveElements(paragraph).length,
|
|
1585
|
+
paragraphArtifactLabelCount: Array.from(new Set(paragraphArtifactLabels)).length,
|
|
1586
|
+
paragraphOtherTextLength: otherTextLength(paragraph?.textContent, [...paragraphArtifactLabels, ...interactiveLabels(paragraph)]),
|
|
1587
|
+
listItemInteractiveCount: interactiveElements(listItem).length,
|
|
1588
|
+
listItemArtifactLabelCount: Array.from(new Set(listItemArtifactLabels)).length,
|
|
1589
|
+
focusableInteractiveCount: interactiveElements(focusable).length,
|
|
1590
|
+
focusableArtifactLabelCount: Array.from(new Set(focusableArtifactLabels)).length,
|
|
1591
|
+
focusableOtherTextLength: otherTextLength(focusable?.textContent, [...focusableArtifactLabels, ...interactiveLabels(focusable)]),
|
|
1550
1592
|
};
|
|
1551
1593
|
})
|
|
1552
1594
|
.filter(Boolean);
|
|
@@ -1555,10 +1597,26 @@ async function collectArtifactCandidates(job, responseIndex, responseText = "")
|
|
|
1555
1597
|
`),
|
|
1556
1598
|
);
|
|
1557
1599
|
|
|
1600
|
+
const partitioned = partitionStructuralArtifactCandidates(structural?.candidates || []);
|
|
1601
|
+
const snapshotEntries = parseSnapshotEntries(targetSlice);
|
|
1602
|
+
const hasGenericArtifactControl = snapshotEntries.some(
|
|
1603
|
+
(entry) =>
|
|
1604
|
+
(entry.kind === "button" || entry.kind === "link") &&
|
|
1605
|
+
!entry.disabled &&
|
|
1606
|
+
/(?:^|\b)(?:download|save)(?:\b|$)/i.test(`${entry.label || ""} ${entry.value || ""}`),
|
|
1607
|
+
);
|
|
1608
|
+
const suspiciousFromText = hasGenericArtifactControl
|
|
1609
|
+
? extractArtifactLabels(responseText)
|
|
1610
|
+
.filter((label) => !partitioned.confirmed.some((candidate) => candidate.label === label) && !partitioned.suspicious.some((candidate) => candidate.label === label))
|
|
1611
|
+
.map((label) => ({ label }))
|
|
1612
|
+
: [];
|
|
1613
|
+
|
|
1558
1614
|
return {
|
|
1559
1615
|
snapshot,
|
|
1560
1616
|
targetSlice,
|
|
1561
|
-
candidates:
|
|
1617
|
+
candidates: partitioned.confirmed,
|
|
1618
|
+
suspiciousLabels: [...partitioned.suspicious.map((candidate) => candidate.label), ...suspiciousFromText.map((candidate) => candidate.label)]
|
|
1619
|
+
.filter((label, index, labels) => labels.indexOf(label) === index),
|
|
1562
1620
|
};
|
|
1563
1621
|
}
|
|
1564
1622
|
|
|
@@ -1566,11 +1624,14 @@ async function waitForStableArtifactCandidates(job, responseIndex, responseText
|
|
|
1566
1624
|
const deadline = Date.now() + ARTIFACT_CANDIDATE_STABILITY_TIMEOUT_MS;
|
|
1567
1625
|
let lastSignature;
|
|
1568
1626
|
let stablePolls = 0;
|
|
1569
|
-
let latest = { snapshot: "", targetSlice: undefined, candidates: [] };
|
|
1627
|
+
let latest = { snapshot: "", targetSlice: undefined, candidates: [], suspiciousLabels: [] };
|
|
1570
1628
|
|
|
1571
1629
|
while (Date.now() < deadline) {
|
|
1572
1630
|
latest = await collectArtifactCandidates(job, responseIndex, responseText);
|
|
1573
|
-
const signature =
|
|
1631
|
+
const signature = JSON.stringify({
|
|
1632
|
+
candidates: latest.candidates.map((candidate) => candidate.label),
|
|
1633
|
+
suspiciousLabels: latest.suspiciousLabels,
|
|
1634
|
+
});
|
|
1574
1635
|
if (signature === lastSignature) stablePolls += 1;
|
|
1575
1636
|
else {
|
|
1576
1637
|
lastSignature = signature;
|
|
@@ -1628,7 +1689,7 @@ async function downloadArtifacts(job, responseIndex, responseText = "") {
|
|
|
1628
1689
|
return [];
|
|
1629
1690
|
}
|
|
1630
1691
|
|
|
1631
|
-
|
|
1692
|
+
let { targetSlice, candidates, suspiciousLabels } = await reopenConversationForArtifacts(job, responseIndex, responseText, "initial");
|
|
1632
1693
|
if (!targetSlice) {
|
|
1633
1694
|
await log(`No assistant response found in snapshot for response index ${responseIndex}`);
|
|
1634
1695
|
await secureWriteText(`${jobDir}/artifacts.json`, "[]\n");
|
|
@@ -1637,33 +1698,32 @@ async function downloadArtifacts(job, responseIndex, responseText = "") {
|
|
|
1637
1698
|
}
|
|
1638
1699
|
|
|
1639
1700
|
await log(`Artifact candidates: ${candidates.map((candidate) => candidate.label).join(", ") || "(none)"}`);
|
|
1701
|
+
if (suspiciousLabels.length > 0) {
|
|
1702
|
+
await log(`Suspicious artifact signals: ${suspiciousLabels.join(", ")}`);
|
|
1703
|
+
}
|
|
1640
1704
|
|
|
1641
1705
|
const artifactsDir = `${jobDir}/artifacts`;
|
|
1642
1706
|
await ensurePrivateDir(artifactsDir);
|
|
1643
1707
|
const artifacts = [];
|
|
1644
1708
|
await flushArtifactsState(artifacts);
|
|
1645
1709
|
|
|
1646
|
-
for (const [index,
|
|
1710
|
+
for (const [index, originalCandidate] of candidates.entries()) {
|
|
1647
1711
|
let downloaded = false;
|
|
1712
|
+
let activeCandidate = originalCandidate;
|
|
1648
1713
|
for (let attempt = 1; attempt <= ARTIFACT_DOWNLOAD_MAX_ATTEMPTS && !downloaded; attempt += 1) {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
const entry = freshEntries.find(
|
|
1654
|
-
(artifactEntry) => artifactEntry.label === candidate.label && (artifactEntry.kind === "button" || artifactEntry.kind === "link") && !artifactEntry.disabled,
|
|
1655
|
-
);
|
|
1656
|
-
if (!entry) {
|
|
1657
|
-
await log(`Artifact "${candidate.label}" not found in fresh snapshot, skipping`);
|
|
1714
|
+
if (!activeCandidate?.selector) {
|
|
1715
|
+
await log(`Artifact "${originalCandidate.label}" has no live selector, marking unconfirmed`);
|
|
1716
|
+
artifacts.push({ displayName: originalCandidate.label, unconfirmed: true, error: "Artifact candidate lost its live selector before download." });
|
|
1717
|
+
await flushArtifactsState(artifacts);
|
|
1658
1718
|
break;
|
|
1659
1719
|
}
|
|
1660
1720
|
|
|
1661
|
-
const destinationPath = join(artifactsDir, preferredArtifactName(
|
|
1721
|
+
const destinationPath = join(artifactsDir, preferredArtifactName(originalCandidate.label, index));
|
|
1662
1722
|
await rm(destinationPath, { force: true }).catch(() => undefined);
|
|
1663
1723
|
try {
|
|
1664
|
-
await log(`Artifact "${
|
|
1724
|
+
await log(`Artifact "${originalCandidate.label}" download attempt ${attempt}/${ARTIFACT_DOWNLOAD_MAX_ATTEMPTS} using selector ${activeCandidate.selector}`);
|
|
1665
1725
|
await withHeartbeatWhile(() =>
|
|
1666
|
-
agentBrowser(job, "download",
|
|
1726
|
+
agentBrowser(job, "download", activeCandidate.selector, destinationPath, {
|
|
1667
1727
|
timeoutMs: ARTIFACT_DOWNLOAD_TIMEOUT_MS,
|
|
1668
1728
|
}),
|
|
1669
1729
|
);
|
|
@@ -1675,7 +1735,7 @@ async function downloadArtifacts(job, responseIndex, responseText = "") {
|
|
|
1675
1735
|
detectType(destinationPath),
|
|
1676
1736
|
]);
|
|
1677
1737
|
artifacts.push({
|
|
1678
|
-
displayName:
|
|
1738
|
+
displayName: originalCandidate.label,
|
|
1679
1739
|
fileName: basename(destinationPath),
|
|
1680
1740
|
copiedPath: destinationPath,
|
|
1681
1741
|
size,
|
|
@@ -1686,11 +1746,15 @@ async function downloadArtifacts(job, responseIndex, responseText = "") {
|
|
|
1686
1746
|
} catch (error) {
|
|
1687
1747
|
const message = error instanceof Error ? error.message : String(error);
|
|
1688
1748
|
await rm(destinationPath, { force: true }).catch(() => undefined);
|
|
1689
|
-
await log(`Artifact "${
|
|
1749
|
+
await log(`Artifact "${originalCandidate.label}" download failed on attempt ${attempt}/${ARTIFACT_DOWNLOAD_MAX_ATTEMPTS}: ${message}`);
|
|
1690
1750
|
if (attempt >= ARTIFACT_DOWNLOAD_MAX_ATTEMPTS) {
|
|
1691
|
-
artifacts.push({ displayName:
|
|
1751
|
+
artifacts.push({ displayName: originalCandidate.label, unconfirmed: true, error: message });
|
|
1692
1752
|
} else {
|
|
1693
|
-
await reopenConversationForArtifacts(job, responseIndex, responseText, `retry ${attempt + 1} for ${
|
|
1753
|
+
const refreshed = await reopenConversationForArtifacts(job, responseIndex, responseText, `retry ${attempt + 1} for ${originalCandidate.label}`);
|
|
1754
|
+
targetSlice = refreshed.targetSlice;
|
|
1755
|
+
candidates = refreshed.candidates;
|
|
1756
|
+
suspiciousLabels = refreshed.suspiciousLabels;
|
|
1757
|
+
activeCandidate = candidates.find((candidate) => candidate.label === originalCandidate.label);
|
|
1694
1758
|
await sleep(1_000);
|
|
1695
1759
|
}
|
|
1696
1760
|
} finally {
|
|
@@ -1699,6 +1763,16 @@ async function downloadArtifacts(job, responseIndex, responseText = "") {
|
|
|
1699
1763
|
}
|
|
1700
1764
|
}
|
|
1701
1765
|
|
|
1766
|
+
const capturedArtifactLabels = new Set(artifacts.map((artifact) => artifact.displayName).filter(Boolean));
|
|
1767
|
+
const missedArtifactLabels = suspiciousLabels.filter((label) => !capturedArtifactLabels.has(label));
|
|
1768
|
+
if (missedArtifactLabels.length > 0) {
|
|
1769
|
+
await log(`Marking missed artifact signals as unconfirmed: ${missedArtifactLabels.join(", ")}`);
|
|
1770
|
+
for (const label of missedArtifactLabels) {
|
|
1771
|
+
artifacts.push({ displayName: label, unconfirmed: true, error: "Response-local artifact signal was present, but no downloadable artifact was captured." });
|
|
1772
|
+
}
|
|
1773
|
+
await flushArtifactsState(artifacts);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1702
1776
|
return artifacts;
|
|
1703
1777
|
}
|
|
1704
1778
|
|
|
@@ -1759,9 +1833,10 @@ async function run() {
|
|
|
1759
1833
|
currentJob = await mutateJob((job) => ({ ...job, ...phasePatch("downloading_artifacts", { heartbeatAt: new Date().toISOString() }) }));
|
|
1760
1834
|
const artifacts = await downloadArtifacts(currentJob, completion.responseIndex, completion.responseText);
|
|
1761
1835
|
const artifactFailureCount = artifacts.filter((artifact) => artifact.unconfirmed || artifact.error).length;
|
|
1836
|
+
const finalPhase = artifactFailureCount > 0 ? "complete_with_artifact_errors" : "complete";
|
|
1762
1837
|
|
|
1763
1838
|
await heartbeat(
|
|
1764
|
-
phasePatch(
|
|
1839
|
+
phasePatch(finalPhase, {
|
|
1765
1840
|
status: "complete",
|
|
1766
1841
|
completedAt: new Date().toISOString(),
|
|
1767
1842
|
responsePath: currentJob.responsePath,
|
|
@@ -1773,7 +1848,7 @@ async function run() {
|
|
|
1773
1848
|
);
|
|
1774
1849
|
const persistedJob = await readJob().catch(() => undefined);
|
|
1775
1850
|
await log(`Persisted final status after completion write: ${persistedJob?.status || "unknown"}`);
|
|
1776
|
-
await log(`Job ${currentJob.id} complete`);
|
|
1851
|
+
await log(`Job ${currentJob.id} complete (${finalPhase}, artifact failures=${artifactFailureCount})`);
|
|
1777
1852
|
} catch (error) {
|
|
1778
1853
|
if (!shuttingDown) {
|
|
1779
1854
|
const message = error instanceof Error ? error.message : String(error);
|