agents-relay 1.0.23 → 1.0.24
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/cli.js +7 -2
- package/dist/reconciler.js +7 -0
- package/dist/scheduler.js +1 -1
- package/package.json +1 -1
- package/skills/chatgpt-browser-worker/scripts/_submit_readiness.py +30 -0
- package/skills/chatgpt-browser-worker/scripts/_temporary_bh.py +128 -24
- package/skills/chatgpt-browser-worker/scripts/temporary_bh.py +4 -1
package/dist/cli.js
CHANGED
|
@@ -572,8 +572,8 @@ async function main() {
|
|
|
572
572
|
task.id = arg(args, '--task-id', randomUUID());
|
|
573
573
|
await store.appendTask(task);
|
|
574
574
|
job.tasks.push(task);
|
|
575
|
-
if (job.state
|
|
576
|
-
job.state = '
|
|
575
|
+
if (job.state !== 'RUNNING') {
|
|
576
|
+
job.state = 'RUNNING';
|
|
577
577
|
job.updatedAt = now;
|
|
578
578
|
await store.saveJob(job);
|
|
579
579
|
}
|
|
@@ -597,6 +597,11 @@ async function main() {
|
|
|
597
597
|
task.error = command === 'retry' ? null : 'Cancelled by user';
|
|
598
598
|
task.updatedAt = new Date().toISOString();
|
|
599
599
|
await store.saveTask(task);
|
|
600
|
+
if (command === 'retry' && job.state !== 'RUNNING') {
|
|
601
|
+
job.state = 'RUNNING';
|
|
602
|
+
job.updatedAt = task.updatedAt;
|
|
603
|
+
await store.saveJob(job);
|
|
604
|
+
}
|
|
600
605
|
await saveLocal(localFile, job);
|
|
601
606
|
await publishWake(bus, job, task.id, `${task.id} ${command} requested`);
|
|
602
607
|
console.log(`${id}: ${task.state}`);
|
package/dist/reconciler.js
CHANGED
|
@@ -61,6 +61,7 @@ export class Reconciler {
|
|
|
61
61
|
terminalTimers = new Map();
|
|
62
62
|
terminalExitGraceTimers = new Map();
|
|
63
63
|
continuationLive = new Set();
|
|
64
|
+
launchFailureClaims = new Set();
|
|
64
65
|
terminalWaiters = new Map();
|
|
65
66
|
reconciling = false;
|
|
66
67
|
constructor(store, options) {
|
|
@@ -507,6 +508,12 @@ export class Reconciler {
|
|
|
507
508
|
live.workerStarted = true;
|
|
508
509
|
}
|
|
509
510
|
async failLaunch(jobId, taskId, executionId, message, phase = 'launch') {
|
|
511
|
+
const claim = executionId ?? `${jobId}:${taskId}`;
|
|
512
|
+
if (this.launchFailureClaims.has(claim))
|
|
513
|
+
return;
|
|
514
|
+
this.launchFailureClaims.add(claim);
|
|
515
|
+
const cleanup = setTimeout(() => this.launchFailureClaims.delete(claim), 60000);
|
|
516
|
+
cleanup.unref();
|
|
510
517
|
const job = await this.store.load(jobId);
|
|
511
518
|
const task = job.tasks.find(item => item.id === taskId);
|
|
512
519
|
if (!task || task.executionId !== executionId || task.state !== 'RUNNING')
|
package/dist/scheduler.js
CHANGED
|
@@ -31,7 +31,7 @@ export function schedule(job) {
|
|
|
31
31
|
const waiting = job.tasks.some(t => ['QUEUED', 'WAITING'].includes(t.state));
|
|
32
32
|
const failed = job.tasks.some(t => t.state === 'FAILED');
|
|
33
33
|
const cancelled = job.tasks.every(t => t.state === 'CANCELLED');
|
|
34
|
-
const next =
|
|
34
|
+
const next = active ? 'RUNNING' : failed ? 'FAILED' : blocked ? 'BLOCKED' : waiting ? 'WAITING' : cancelled ? 'CANCELLED' : completionGateSatisfied(job) ? 'COMPLETED' : 'WAITING';
|
|
35
35
|
if (job.state !== next) {
|
|
36
36
|
assertJobTransition(job.state, next);
|
|
37
37
|
job.state = next;
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Bounded polling helpers for ChatGPT submit readiness."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def wait_until_stable(
|
|
9
|
+
read_state: Callable[[], Mapping[str, object]],
|
|
10
|
+
ready: Callable[[Mapping[str, object]], bool],
|
|
11
|
+
*,
|
|
12
|
+
timeout: float,
|
|
13
|
+
interval: float = 0.25,
|
|
14
|
+
stable_samples: int = 2,
|
|
15
|
+
phase: str,
|
|
16
|
+
) -> Mapping[str, object]:
|
|
17
|
+
"""Return only after the observable state is ready for stable_samples polls."""
|
|
18
|
+
deadline = time.monotonic() + timeout
|
|
19
|
+
stable = 0
|
|
20
|
+
last_state: Mapping[str, object] = {}
|
|
21
|
+
while time.monotonic() < deadline:
|
|
22
|
+
last_state = read_state()
|
|
23
|
+
if ready(last_state):
|
|
24
|
+
stable += 1
|
|
25
|
+
if stable >= stable_samples:
|
|
26
|
+
return last_state
|
|
27
|
+
else:
|
|
28
|
+
stable = 0
|
|
29
|
+
time.sleep(interval)
|
|
30
|
+
raise RuntimeError(f"Temporary Chat {phase} was not observed ready")
|
|
@@ -58,6 +58,21 @@ def _composer_text(selector):
|
|
|
58
58
|
}})()""") or ""
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def _wait_for_composer(timeout=30):
|
|
62
|
+
def read_selector():
|
|
63
|
+
try:
|
|
64
|
+
return _composer()
|
|
65
|
+
except RuntimeError:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
return wait_until_stable(
|
|
69
|
+
lambda: {"selector": read_selector()},
|
|
70
|
+
lambda state: bool(state["selector"]),
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
phase="composer readiness",
|
|
73
|
+
)["selector"]
|
|
74
|
+
|
|
75
|
+
|
|
61
76
|
def _same_text(observed, expected):
|
|
62
77
|
normalize = lambda value: re.sub(r"\\s+", " ", value or "").strip()
|
|
63
78
|
return normalize(expected) in normalize(observed)
|
|
@@ -73,6 +88,38 @@ def _attachment_names():
|
|
|
73
88
|
.filter(Boolean))()""") or []
|
|
74
89
|
|
|
75
90
|
|
|
91
|
+
def _attachment_state():
|
|
92
|
+
return js(r"""(() => {
|
|
93
|
+
const names = [...document.querySelectorAll('button[aria-label^="Remove file"]')]
|
|
94
|
+
.map(b => (b.getAttribute('aria-label') || '').replace(/^Remove file\s+\d+:\s*/, ''))
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
const pending = [...document.querySelectorAll(
|
|
97
|
+
'[role="progressbar"], [aria-busy="true"], [data-state="loading"]'
|
|
98
|
+
)].some(e => {
|
|
99
|
+
const r=e.getBoundingClientRect();
|
|
100
|
+
return r.width > 0 && r.height > 0;
|
|
101
|
+
});
|
|
102
|
+
return {names, pending};
|
|
103
|
+
})()""") or {"names": [], "pending": True}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _send_state():
|
|
107
|
+
return js(r"""(() => {
|
|
108
|
+
const b=document.querySelector('button[data-testid="send-button"],button[aria-label="Send prompt"]');
|
|
109
|
+
return {present: !!b, enabled: !!b && !b.disabled && b.getAttribute('aria-disabled') !== 'true'};
|
|
110
|
+
})()""") or {"present": False, "enabled": False}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _wait_for_attachments(expected, timeout=45):
|
|
114
|
+
expected = set(expected)
|
|
115
|
+
wait_until_stable(
|
|
116
|
+
_attachment_state,
|
|
117
|
+
lambda state: expected.issubset(state["names"]) and not state["pending"],
|
|
118
|
+
timeout=timeout,
|
|
119
|
+
phase="attachment upload readiness",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
76
123
|
def _upload_files(paths):
|
|
77
124
|
if not paths:
|
|
78
125
|
return []
|
|
@@ -87,13 +134,17 @@ def _upload_files(paths):
|
|
|
87
134
|
if not os.path.isfile(path):
|
|
88
135
|
raise RuntimeError(f"attachment does not exist: {path}")
|
|
89
136
|
upload_file(selector, path)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
137
|
+
expected = [os.path.basename(path) for path in paths]
|
|
138
|
+
if expected:
|
|
139
|
+
# Presence is a selection signal; upload readiness is checked separately.
|
|
140
|
+
wait_until_stable(
|
|
141
|
+
_attachment_state,
|
|
142
|
+
lambda state: set(expected).issubset(state["names"]),
|
|
143
|
+
timeout=30,
|
|
144
|
+
phase="attachment presence",
|
|
145
|
+
)
|
|
146
|
+
_wait_for_attachments(expected)
|
|
147
|
+
return expected
|
|
97
148
|
|
|
98
149
|
|
|
99
150
|
def _user_turns():
|
|
@@ -109,17 +160,41 @@ def _click_send():
|
|
|
109
160
|
b.click(); return true;
|
|
110
161
|
})()""")
|
|
111
162
|
if not ok:
|
|
112
|
-
raise RuntimeError("Temporary Chat
|
|
163
|
+
raise RuntimeError("Temporary Chat send readiness changed before click")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _wait_for_send_ready(selector, prompt, attachments):
|
|
167
|
+
expected = set(attachments)
|
|
168
|
+
|
|
169
|
+
def read_state():
|
|
170
|
+
return {
|
|
171
|
+
"text": _composer_text(selector),
|
|
172
|
+
"attachments": _attachment_state(),
|
|
173
|
+
"send": _send_state(),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
wait_until_stable(
|
|
177
|
+
read_state,
|
|
178
|
+
lambda state: (
|
|
179
|
+
_normalized_text(prompt) in _normalized_text(state["text"])
|
|
180
|
+
and expected.issubset(state["attachments"]["names"])
|
|
181
|
+
and not state["attachments"]["pending"]
|
|
182
|
+
and state["send"]["enabled"]
|
|
183
|
+
),
|
|
184
|
+
timeout=45 if expected else 15,
|
|
185
|
+
phase="send readiness",
|
|
186
|
+
)
|
|
113
187
|
|
|
114
188
|
|
|
115
189
|
def _wait_user_turn(before_count, prompt, timeout=20):
|
|
116
190
|
deadline = time.time() + timeout
|
|
117
191
|
while time.time() < deadline:
|
|
118
192
|
turns = _user_turns()
|
|
119
|
-
|
|
120
|
-
|
|
193
|
+
for turn in turns[before_count:]:
|
|
194
|
+
if _normalized_text(prompt) in _normalized_text(turn["text"]):
|
|
195
|
+
return turn
|
|
121
196
|
time.sleep(.25)
|
|
122
|
-
raise RuntimeError("Temporary Chat
|
|
197
|
+
raise RuntimeError("Temporary Chat submission verification did not observe a new user turn")
|
|
123
198
|
|
|
124
199
|
|
|
125
200
|
def _diagnostic_thread_id():
|
|
@@ -145,30 +220,59 @@ deadline = time.time() + 20
|
|
|
145
220
|
while time.time() < deadline:
|
|
146
221
|
if "temporary-chat=true" in page_info().get("url", ""):
|
|
147
222
|
try:
|
|
148
|
-
_composer()
|
|
149
223
|
break
|
|
150
224
|
except RuntimeError:
|
|
151
225
|
pass
|
|
152
226
|
time.sleep(.25)
|
|
153
227
|
else:
|
|
154
|
-
raise RuntimeError("Temporary Chat
|
|
228
|
+
raise RuntimeError("Temporary Chat composer readiness was not observed")
|
|
155
229
|
|
|
156
230
|
attachments = _upload_files(CFG.get("file", []))
|
|
157
|
-
selector =
|
|
231
|
+
selector = _wait_for_composer()
|
|
158
232
|
before_count = len(_user_turns())
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
233
|
+
prompt = CFG["prompt"]
|
|
234
|
+
is_contenteditable = bool(js(f"""(() => {{
|
|
235
|
+
const e=document.querySelector({json.dumps(selector)});
|
|
236
|
+
return !!e && e.getAttribute('contenteditable') === 'true';
|
|
237
|
+
}})()"""))
|
|
238
|
+
if is_contenteditable:
|
|
239
|
+
cleared = js(f"""(() => {{
|
|
162
240
|
const e=document.querySelector({json.dumps(selector)});
|
|
163
|
-
|
|
164
|
-
|
|
241
|
+
if (!e) return false;
|
|
242
|
+
e.focus();
|
|
243
|
+
const sel=window.getSelection();
|
|
244
|
+
const range=document.createRange();
|
|
245
|
+
range.selectNodeContents(e);
|
|
246
|
+
sel.removeAllRanges();
|
|
247
|
+
sel.addRange(range);
|
|
248
|
+
document.execCommand('delete', false, null);
|
|
249
|
+
return true;
|
|
165
250
|
}})()""")
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
251
|
+
if not cleared:
|
|
252
|
+
raise RuntimeError("Temporary Chat contenteditable composer could not be cleared")
|
|
253
|
+
for offset in range(0, len(prompt), 256):
|
|
254
|
+
chunk = prompt[offset:offset + 256]
|
|
255
|
+
inserted = js(f"""(() => {{
|
|
256
|
+
const e=document.querySelector({json.dumps(selector)});
|
|
257
|
+
if (!e) return false;
|
|
258
|
+
e.focus();
|
|
259
|
+
const ok=document.execCommand('insertText', false, {json.dumps(chunk)});
|
|
260
|
+
e.dispatchEvent(new InputEvent('input', {{bubbles:true,inputType:'insertText',data:{json.dumps(chunk)}}}));
|
|
261
|
+
return ok;
|
|
262
|
+
}})()""")
|
|
263
|
+
if not inserted:
|
|
264
|
+
raise RuntimeError(f"Temporary Chat composer rejected prompt chunk at offset {offset}")
|
|
265
|
+
else:
|
|
266
|
+
fill_input(selector, prompt, clear_first=True)
|
|
267
|
+
|
|
268
|
+
wait_until_stable(
|
|
269
|
+
lambda: {"text": _composer_text(selector)},
|
|
270
|
+
lambda state: _normalized_text(CFG["prompt"]) in _normalized_text(state["text"]),
|
|
271
|
+
timeout=30,
|
|
272
|
+
phase="composer readiness",
|
|
273
|
+
)
|
|
171
274
|
|
|
275
|
+
_wait_for_send_ready(selector, CFG["prompt"], attachments)
|
|
172
276
|
_click_send()
|
|
173
277
|
user_turn = _wait_user_turn(before_count, CFG["prompt"])
|
|
174
278
|
|
|
@@ -10,6 +10,7 @@ import tempfile
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
BH_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_temporary_bh.py")
|
|
13
|
+
READINESS_HELPER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_submit_readiness.py")
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def main() -> int:
|
|
@@ -23,7 +24,9 @@ def main() -> int:
|
|
|
23
24
|
json.dump(vars(args), handle, ensure_ascii=False)
|
|
24
25
|
config_path = handle.name
|
|
25
26
|
try:
|
|
26
|
-
|
|
27
|
+
helper = open(READINESS_HELPER, encoding="utf-8").read()
|
|
28
|
+
code = helper + "\n" + open(BH_SCRIPT, encoding="utf-8").read()
|
|
29
|
+
code = code.replace("__CFG_PATH__", config_path)
|
|
27
30
|
return subprocess.run(["browser-harness"], input=code, text=True, timeout=180).returncode
|
|
28
31
|
finally:
|
|
29
32
|
os.unlink(config_path)
|