mtech-ai-local-runner 0.1.55 → 0.1.56
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/README.md +15 -0
- package/dist/src/asks/hook-client.js +31 -4
- package/dist/src/asks/hub-protocol.js +8 -1
- package/dist/src/asks/inbox.js +1 -1
- package/dist/src/asks/routing.js +31 -5
- package/dist/src/asks/store.js +4 -0
- package/dist/src/daemon/job-poller.js +42 -0
- package/dist/src/daemon/session-open-poller.js +123 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 0.1.56: AI-first asks count only prompts a person typed
|
|
2
|
+
|
|
3
|
+
Claude Code runs UserPromptSubmit for turns nobody typed as well: an asyncRewake wake, a task notification, a message
|
|
4
|
+
from another session, a side session's brief (Work Item e1c08b33, Work Decision cc8817f9). The ask hook client now
|
|
5
|
+
classifies each prompt on this Mac and tells the hub only whether a person typed it, never the text.
|
|
6
|
+
- A prompt nobody typed marks the session as in a turn and does nothing else: it does not show an ask, does not count one
|
|
7
|
+
as seen and does not make the session the one asks go to.
|
|
8
|
+
- A typed prompt within 10 s of a wake or a stop show does not count that ask as seen.
|
|
9
|
+
- An ask pinned to a session in a turn is shown at the next typed prompt in any session that becomes its chosen session
|
|
10
|
+
(PR #421), or at that session's turn end.
|
|
11
|
+
|
|
12
|
+
The hub still accepts the prompt events of an older hook client and counts them as typed. `asks.json` gains an optional
|
|
13
|
+
`lastTurnStartAt` per session. A session that was already open before the integration was installed has no hooks: start
|
|
14
|
+
a new session, or run `/reload-plugins` in the terminal.
|
|
15
|
+
|
|
1
16
|
## 0.1.55: guide hooks that steer AI sessions to the AI Gateway (disabled)
|
|
2
17
|
|
|
3
18
|
The guide integration (Work Item d38d60f9) adds two hooks to Claude Code and Codex. Install it only on request:
|
|
@@ -56,6 +56,32 @@ export function isHumanSession(app, env, _input, stateRoot) {
|
|
|
56
56
|
return attended === "1";
|
|
57
57
|
return INTERACTIVE_ENTRYPOINTS.has(env.CLAUDE_CODE_ENTRYPOINT ?? "");
|
|
58
58
|
}
|
|
59
|
+
/** The reminders a client app puts in front of what was submitted, typed or not. */
|
|
60
|
+
const LEADING_REMINDERS = /^\s*(?:<system-reminder>[\s\S]*?<\/system-reminder>\s*)+/;
|
|
61
|
+
/**
|
|
62
|
+
* How a turn nobody typed begins (Work Decision cc8817f9), as the 2026-10-26 transcripts recorded them. Claude Code runs
|
|
63
|
+
* UserPromptSubmit for these turns as well, and its hook input says nothing of where a prompt came from.
|
|
64
|
+
* - A Claude asyncRewake wake and a background task: a task notification.
|
|
65
|
+
* - The app's report of a side session's turn, and a message from another session.
|
|
66
|
+
* - A side session's first brief, written by the session that started it.
|
|
67
|
+
* - The summary that continues a compacted conversation, and an interruption marker.
|
|
68
|
+
* - This runner's own lines.
|
|
69
|
+
*/
|
|
70
|
+
const NOT_TYPED_PREFIXES = [
|
|
71
|
+
"<task-notification>", "<side-sessions-event>", "<cross-session-message", "<agent-message", "Another Claude session sent a message:",
|
|
72
|
+
"Background (from the session that started this one):", "This session is being continued from a previous conversation",
|
|
73
|
+
"[Request interrupted by user", "AI Gateway:",
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* True when a person typed this prompt. Only the answer leaves this client, never the text. A prompt that is not a
|
|
77
|
+
* string counts as typed, as every prompt did before this rule.
|
|
78
|
+
*/
|
|
79
|
+
export function isHumanPrompt(prompt) {
|
|
80
|
+
if (typeof prompt !== "string")
|
|
81
|
+
return true;
|
|
82
|
+
const rest = prompt.replace(LEADING_REMINDERS, "").trimStart();
|
|
83
|
+
return rest !== "" && !NOT_TYPED_PREFIXES.some(prefix => rest.startsWith(prefix));
|
|
84
|
+
}
|
|
59
85
|
/** Same rule as defaultStateRootFromEnv in src/config.ts, repeated so this client loads nothing else. */
|
|
60
86
|
function stateRootFrom(env) {
|
|
61
87
|
return env.XDG_STATE_HOME ? join(env.XDG_STATE_HOME, "mtec-ai-local-runner") : join(env.HOME ?? "", ".local", "state", "mtec-ai-local-runner");
|
|
@@ -107,7 +133,7 @@ function parseRecord(raw) {
|
|
|
107
133
|
}
|
|
108
134
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
109
135
|
}
|
|
110
|
-
/** Takes the session id, the cwd and Codex's stop_hook_active. Every other field
|
|
136
|
+
/** Takes the session id, the cwd and Codex's stop_hook_active. Every other field is ignored; the prompt is only classified (isHumanPrompt). */
|
|
111
137
|
function hookFields(input) {
|
|
112
138
|
const { session_id: sessionId, cwd, stop_hook_active: stopHookActive } = input;
|
|
113
139
|
if (typeof sessionId !== "string" || !SESSION_ID.test(sessionId))
|
|
@@ -194,9 +220,10 @@ export async function runHookClient(argv, io) {
|
|
|
194
220
|
io.stderr(text);
|
|
195
221
|
return 2;
|
|
196
222
|
}
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
:
|
|
223
|
+
const base = { v: 1, op: "event", app: args.app, event: args.event, session_id: input.sessionId, cwd: input.cwd };
|
|
224
|
+
const request = args.event === "stop" ? { ...base, stop_hook_active: input.stopHookActive }
|
|
225
|
+
: args.event === "prompt" ? { ...base, human: isHumanPrompt(record.prompt) }
|
|
226
|
+
: base;
|
|
200
227
|
const text = await converse(socketPath, encodeLine(request), hubTimeoutMs, hubTimeoutMs, line => ({ done: true, value: parseEventResponse(line)?.text ?? null }));
|
|
201
228
|
if (text === null || text === "")
|
|
202
229
|
return 0;
|
|
@@ -5,7 +5,9 @@ import { MAX_CWD_LENGTH, SESSION_ID, isApp, isHookEvent } from "./types.js";
|
|
|
5
5
|
* may import nothing but ./types.js.
|
|
6
6
|
*
|
|
7
7
|
* Client to hub, one line, then the client waits without closing its side:
|
|
8
|
-
* {"v":1,"op":"event","app":"claude"|"codex","event":"session-start"|"
|
|
8
|
+
* {"v":1,"op":"event","app":"claude"|"codex","event":"session-start"|"session-end","session_id":…,"cwd":…}
|
|
9
|
+
* {"v":1,"op":"event","app":"claude"|"codex","event":"prompt","session_id":…,"cwd":…,"human":true|false}
|
|
10
|
+
* (a client from before the flag sends no "human"; that prompt counts as typed)
|
|
9
11
|
* {"v":1,"op":"event","app":"codex","event":"stop","session_id":…,"cwd":…,"stop_hook_active":false}
|
|
10
12
|
* {"v":1,"op":"watch","app":"claude","session_id":…,"cwd":…} (the Claude Stop watcher)
|
|
11
13
|
* {"v":1,"op":"status"} (doctor)
|
|
@@ -68,6 +70,11 @@ export function parseHubRequest(line) {
|
|
|
68
70
|
return null;
|
|
69
71
|
return { v: 1, op: "event", app: "codex", event: "stop", session_id: value.session_id, cwd: value.cwd, stop_hook_active: value.stop_hook_active };
|
|
70
72
|
}
|
|
73
|
+
if (value.event === "prompt" && Object.keys(value).length === 7) {
|
|
74
|
+
if (typeof value.human !== "boolean")
|
|
75
|
+
return null;
|
|
76
|
+
return { v: 1, op: "event", app: value.app, event: "prompt", session_id: value.session_id, cwd: value.cwd, human: value.human };
|
|
77
|
+
}
|
|
71
78
|
if (Object.keys(value).length !== 6)
|
|
72
79
|
return null;
|
|
73
80
|
return { v: 1, op: "event", app: value.app, event: value.event, session_id: value.session_id, cwd: value.cwd };
|
package/dist/src/asks/inbox.js
CHANGED
|
@@ -83,7 +83,7 @@ export class AskInbox {
|
|
|
83
83
|
answer(null);
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
|
-
const context = { app: request.app, sessionId: request.session_id, cwd: request.cwd, now: this.now() };
|
|
86
|
+
const context = { app: request.app, sessionId: request.session_id, cwd: request.cwd, now: this.now(), human: request.human !== false };
|
|
87
87
|
let text = null;
|
|
88
88
|
if (request.event === "session-start")
|
|
89
89
|
text = renderOutput(sessionStarted(this.state, context), { sessionId: request.session_id });
|
package/dist/src/asks/routing.js
CHANGED
|
@@ -19,6 +19,12 @@ export const MAX_ITEMS_PER_OUTPUT = 5;
|
|
|
19
19
|
export const OPEN_PENDING_MS = 15 * 60 * 1000;
|
|
20
20
|
/** At most 3 sessions opened per Mac in any hour. */
|
|
21
21
|
export const OPEN_LIMIT = 3;
|
|
22
|
+
/**
|
|
23
|
+
* A prompt this soon after a wake or a stop show does not count as seen (Work Decision cc8817f9). Claude submits the
|
|
24
|
+
* woken turn itself within a few seconds, and a person needs longer to read the ask and type. An early typed prompt
|
|
25
|
+
* leaves the item shown: the next one counts, or the Gateway's Slack fallback covers it.
|
|
26
|
+
*/
|
|
27
|
+
export const SEEN_AFTER_CONTINUATION_MS = 10_000;
|
|
22
28
|
export function hashSessionId(sessionId) {
|
|
23
29
|
return createHash("sha256").update(sessionId, "utf8").digest("hex");
|
|
24
30
|
}
|
|
@@ -29,8 +35,10 @@ export function isLive(session, now) {
|
|
|
29
35
|
export function humanPromptWithin(session, now) {
|
|
30
36
|
return session.lastPromptAt !== null && now - session.lastPromptAt <= WAKE_WINDOW_MS;
|
|
31
37
|
}
|
|
38
|
+
/** A turn started by a typed prompt or by one nobody typed has not ended yet. */
|
|
32
39
|
export function turnInProgress(session) {
|
|
33
|
-
|
|
40
|
+
const started = Math.max(session.lastPromptAt ?? -1, session.lastTurnStartAt ?? -1);
|
|
41
|
+
return started >= 0 && (session.lastTurnEndAt === null || started > session.lastTurnEndAt);
|
|
34
42
|
}
|
|
35
43
|
export function wakesLeft(session, now) {
|
|
36
44
|
return WAKE_LIMIT - session.wakes.filter(at => now - at < WAKE_PERIOD_MS).length;
|
|
@@ -64,7 +72,8 @@ export function chooseSession(item, state, now) {
|
|
|
64
72
|
* Where a delivery that is not shown yet goes now:
|
|
65
73
|
* - no live session, or the chosen one had no human prompt in 30 minutes: a notification, then the next
|
|
66
74
|
* SessionStart or prompt in any session;
|
|
67
|
-
* - the chosen session is in a turn: pinned there, shown at its turn end or next prompt
|
|
75
|
+
* - the chosen session is in a turn: pinned there, shown at its turn end or at the next prompt in any session that
|
|
76
|
+
* becomes the chosen one by that prompt (see promptSubmitted);
|
|
68
77
|
* - an idle Claude session whose Stop watcher waits, under its hourly limit: woken now;
|
|
69
78
|
* - otherwise (an idle Codex session, no watcher, or the limit reached): a notification.
|
|
70
79
|
*/
|
|
@@ -168,13 +177,30 @@ export function sessionStarted(state, context) {
|
|
|
168
177
|
markShown(item, key, context.app, "session_start", context.now);
|
|
169
178
|
return output;
|
|
170
179
|
}
|
|
171
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* UserPromptSubmit. A prompt nobody typed only starts a turn: it shows nothing, counts nothing as seen and is not a human
|
|
182
|
+
* prompt for routing (Work Decision cc8817f9).
|
|
183
|
+
*
|
|
184
|
+
* A typed prompt makes earlier items shown in this session seen, except those a wake or a stop showed under
|
|
185
|
+
* SEEN_AFTER_CONTINUATION_MS ago. Waiting items are shown and seen at once. So is an item pinned to another session that
|
|
186
|
+
* this prompt makes the chosen session for it: the person just typed here, while that session is still in its turn.
|
|
187
|
+
* Without this the next plan pinned the item here to wait for this turn's end, and a prompt elsewhere could move it on
|
|
188
|
+
* again before then (live test 2026-10-26). An answer that names a live session stays there, because the session that
|
|
189
|
+
* asked is still chosen first.
|
|
190
|
+
*/
|
|
172
191
|
export function promptSubmitted(state, context) {
|
|
173
192
|
const { key, session } = touch(state, context);
|
|
193
|
+
if (context.human === false) {
|
|
194
|
+
session.lastTurnStartAt = context.now;
|
|
195
|
+
return { items: [], more: [] };
|
|
196
|
+
}
|
|
174
197
|
session.lastPromptAt = context.now;
|
|
175
|
-
|
|
198
|
+
const settled = (item) => (item.shownVia !== "rewake" && item.shownVia !== "stop")
|
|
199
|
+
|| item.shownAt === null || context.now - item.shownAt >= SEEN_AFTER_CONTINUATION_MS;
|
|
200
|
+
for (const item of itemsWhere(state, item => item.phase === "shown" && item.sessionKey === key && settled(item)))
|
|
176
201
|
markSeen(item, context.app, context.now);
|
|
177
|
-
const
|
|
202
|
+
const takenOver = itemsWhere(state, item => item.phase === "pinned" && item.sessionKey !== key && chooseSession(item, state, context.now) === key);
|
|
203
|
+
const output = selectForOutput([...dueFor(state, key), ...takenOver]);
|
|
178
204
|
for (const item of everyItem(output)) {
|
|
179
205
|
markShown(item, key, context.app, "prompt", context.now);
|
|
180
206
|
markSeen(item, context.app, context.now);
|
package/dist/src/asks/store.js
CHANGED
|
@@ -100,7 +100,11 @@ function normalizeSession(key, value) {
|
|
|
100
100
|
return null;
|
|
101
101
|
if (!Array.isArray(v.wakes) || !v.wakes.every(isTime))
|
|
102
102
|
return null;
|
|
103
|
+
// Stored before Work Decision cc8817f9 a session has no turn start; one that is not a time is dropped with the session.
|
|
104
|
+
if (v.lastTurnStartAt !== undefined && !isTime(v.lastTurnStartAt))
|
|
105
|
+
return null;
|
|
103
106
|
return { app: v.app, sessionId: v.sessionId, cwd: v.cwd, startedAt: v.startedAt, lastEventAt: v.lastEventAt, lastPromptAt: v.lastPromptAt,
|
|
107
|
+
...(v.lastTurnStartAt === undefined ? {} : { lastTurnStartAt: v.lastTurnStartAt }),
|
|
104
108
|
lastTurnEndAt: v.lastTurnEndAt, endedAt: v.endedAt, wakes: [...v.wakes] };
|
|
105
109
|
}
|
|
106
110
|
/**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class JobPoller {
|
|
2
|
+
deps;
|
|
3
|
+
stopped = false;
|
|
4
|
+
failures = 0;
|
|
5
|
+
constructor(deps) {
|
|
6
|
+
this.deps = deps;
|
|
7
|
+
}
|
|
8
|
+
stop() { this.stopped = true; }
|
|
9
|
+
async tick() {
|
|
10
|
+
if (this.stopped)
|
|
11
|
+
return;
|
|
12
|
+
let stage = "heartbeat";
|
|
13
|
+
try {
|
|
14
|
+
await this.deps.heartbeat?.();
|
|
15
|
+
stage = "wait";
|
|
16
|
+
const claim = await this.deps.wait();
|
|
17
|
+
stage = "execute";
|
|
18
|
+
if (claim.status === "claimed" && claim.job)
|
|
19
|
+
await this.deps.execute(claim.job);
|
|
20
|
+
this.failures = 0;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
try {
|
|
24
|
+
this.deps.reportError?.({ stage, code: stage === "execute" ? "EXECUTION_UNACKNOWLEDGED" : "GATEWAY_UNAVAILABLE" });
|
|
25
|
+
}
|
|
26
|
+
catch { /* Observational logging cannot bypass stop/backoff or discard recovery state. */ }
|
|
27
|
+
// Unknown execution/terminal outcomes must reconcile before any new claim.
|
|
28
|
+
if (stage === "execute") {
|
|
29
|
+
this.stop();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.failures += 1;
|
|
33
|
+
const base = Math.min(30_000, 1000 * (2 ** Math.min(5, this.failures - 1)));
|
|
34
|
+
const jitter = Math.floor(base * 0.2 * (this.deps.random ?? Math.random)());
|
|
35
|
+
await this.deps.delay(base + jitter);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async run() {
|
|
39
|
+
while (!this.stopped)
|
|
40
|
+
await this.tick();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const MINIMUM_CYCLE_MS = 1000;
|
|
2
|
+
/** Pauses before the second and third report attempts. The server treats a repeat report as idempotent. */
|
|
3
|
+
const REPORT_RETRY_DELAYS_MS = [1000, 2000];
|
|
4
|
+
/** The caught error's own failure code (a GatewayToolError code such as GATEWAY_HTTP_401), never its message, which may carry server text. */
|
|
5
|
+
function causeOf(error) {
|
|
6
|
+
const code = error?.code;
|
|
7
|
+
return typeof code === "string" && /^[A-Z0-9_]{1,64}$/.test(code) ? code : undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Resolves after the delay or at once on abort, and never leaves a listener on a long-lived signal. */
|
|
10
|
+
export function abortableDelay(milliseconds, signal) {
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
if (signal.aborted) {
|
|
13
|
+
resolve();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const onAbort = () => { clearTimeout(timer); resolve(); };
|
|
17
|
+
const timer = setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, milliseconds);
|
|
18
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export class SessionOpenPoller {
|
|
22
|
+
deps;
|
|
23
|
+
controller = new AbortController();
|
|
24
|
+
failures = 0;
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
}
|
|
28
|
+
stop() { this.controller.abort(); }
|
|
29
|
+
fail(stage, error, code = `SESSION_OPEN_${stage.toUpperCase()}_FAILED`) {
|
|
30
|
+
const cause = causeOf(error);
|
|
31
|
+
try {
|
|
32
|
+
this.deps.reportError?.({ stage, code, ...(cause ? { cause } : {}) });
|
|
33
|
+
}
|
|
34
|
+
catch { /* Logging cannot stop the loop. */ }
|
|
35
|
+
}
|
|
36
|
+
async pause(milliseconds, signal) {
|
|
37
|
+
await this.deps.delay(milliseconds, signal).catch(() => undefined);
|
|
38
|
+
}
|
|
39
|
+
/** Opens one claimed request and reports it. A failure here never reaches the rest of the batch. */
|
|
40
|
+
async handle(request, signal) {
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
result = await this.deps.open(request);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!signal.aborted)
|
|
47
|
+
this.fail("open", error);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const openId = request?.open_id;
|
|
51
|
+
if (typeof openId !== "string" || openId === "")
|
|
52
|
+
return;
|
|
53
|
+
for (let attempt = 0;; attempt += 1) {
|
|
54
|
+
try {
|
|
55
|
+
await this.deps.report(openId, result, signal);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (signal.aborted)
|
|
60
|
+
return;
|
|
61
|
+
const retryDelay = REPORT_RETRY_DELAYS_MS[attempt];
|
|
62
|
+
if (retryDelay === undefined) {
|
|
63
|
+
this.fail("report", error);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await this.pause(retryDelay, signal);
|
|
67
|
+
if (signal.aborted)
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async tick() {
|
|
73
|
+
const signal = this.controller.signal;
|
|
74
|
+
if (signal.aborted)
|
|
75
|
+
return;
|
|
76
|
+
const now = this.deps.now ?? (() => performance.now());
|
|
77
|
+
const started = now();
|
|
78
|
+
let opens;
|
|
79
|
+
let retryAfterSeconds;
|
|
80
|
+
try {
|
|
81
|
+
({ opens, retryAfterSeconds } = await this.deps.wait(signal));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (signal.aborted)
|
|
85
|
+
return;
|
|
86
|
+
this.fail("wait", error);
|
|
87
|
+
this.failures += 1;
|
|
88
|
+
const base = Math.min(30_000, 1000 * (2 ** Math.min(5, this.failures - 1)));
|
|
89
|
+
const jitter = Math.floor(base * 0.2 * (this.deps.random ?? Math.random)());
|
|
90
|
+
await this.pause(base + jitter, signal);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.failures = 0;
|
|
94
|
+
const claimed = Array.isArray(opens) ? opens : [];
|
|
95
|
+
for (const request of claimed) {
|
|
96
|
+
if (signal.aborted)
|
|
97
|
+
return;
|
|
98
|
+
await this.handle(request, signal);
|
|
99
|
+
}
|
|
100
|
+
if (claimed.length > 0 || signal.aborted)
|
|
101
|
+
return;
|
|
102
|
+
// A wait that returns at once with nothing (a Gateway wait of 0) must not become a hot loop, and while the Gateway's
|
|
103
|
+
// session open switch is off it asks for a longer pause (retry_after_seconds). The clock is monotonic and clamped.
|
|
104
|
+
const elapsed = Math.max(0, now() - started);
|
|
105
|
+
const remainder = elapsed < MINIMUM_CYCLE_MS ? MINIMUM_CYCLE_MS - elapsed : 0;
|
|
106
|
+
const wanted = retryAfterSeconds === undefined ? remainder : Math.max(remainder, retryAfterSeconds * 1000);
|
|
107
|
+
if (wanted > 0)
|
|
108
|
+
await this.pause(wanted, signal);
|
|
109
|
+
}
|
|
110
|
+
async run() {
|
|
111
|
+
const signal = this.controller.signal;
|
|
112
|
+
while (!signal.aborted) {
|
|
113
|
+
try {
|
|
114
|
+
await this.tick();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
this.fail("wait", undefined, "SESSION_OPEN_LOOP_FAILED");
|
|
118
|
+
if (!signal.aborted)
|
|
119
|
+
await this.pause(1000, signal);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|