mtech-ai-local-runner 0.1.74 → 0.1.75
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
CHANGED
|
@@ -525,6 +525,13 @@ publication authority through a late response. Once publication may have taken
|
|
|
525
525
|
effect, retain `publication_pending` recovery instead of falsely completing,
|
|
526
526
|
failing or restarting the attempt. Canonical publication approval remains required.
|
|
527
527
|
|
|
528
|
+
Since 0.1.75 (Work Decision `b3d7eb22`) a failed Gateway stage, advance or fence read
|
|
529
|
+
after stage started is repeated with the same arguments for up to 15 minutes, 5 s
|
|
530
|
+
doubling to 130 s, while the heartbeat keeps the lease. A suspended runner names its
|
|
531
|
+
held publication job in `suspended_job` on sync. Once the Gateway answers that the job
|
|
532
|
+
is closed, it clears its state and restarts. The Gateway closes only that runner's own
|
|
533
|
+
job, only when its lease is long expired and no pull request can exist.
|
|
534
|
+
|
|
528
535
|
Component, shadow admission, installed-package and live-provider evidence are
|
|
529
536
|
separate. Historical 0.1.10 hook behavior below is not current Claude admission;
|
|
530
537
|
Claude parity requires the actual current isolation/admission gate to pass.
|
|
@@ -35,6 +35,7 @@ import { composeReviewedP1Host } from '../verification/p1-host-composition.js';
|
|
|
35
35
|
import { AskRuntime, withAsks } from '../asks/runtime.js';
|
|
36
36
|
import { GuideRuntime, withGuide } from '../guide/runtime.js';
|
|
37
37
|
import { FixConsentRuntime } from './fix-consent.js';
|
|
38
|
+
import { SuspendedPublication } from './suspended-publication.js';
|
|
38
39
|
import { RoomActivityRuntime } from '../rooms/activity.js';
|
|
39
40
|
import { notifyRoomActivity } from '../rooms/notifier.js';
|
|
40
41
|
export async function buildDaemon(input) {
|
|
@@ -267,6 +268,9 @@ export async function buildDaemon(input) {
|
|
|
267
268
|
report: () => localFixHost?.report, jobRunning: () => poller.running,
|
|
268
269
|
restart: () => { daemon.stop(); },
|
|
269
270
|
log: event => { process.stderr.write(`${JSON.stringify(event)}\n`); } });
|
|
271
|
+
// A suspended publication clears itself once the Gateway closed its job (Work Decision b3d7eb22), then restarts like a consent change.
|
|
272
|
+
const suspendedPublication = new SuspendedPublication({ state, restart: () => { daemon.stop(); },
|
|
273
|
+
log: event => { process.stderr.write(`${JSON.stringify(event)}\n`); } });
|
|
270
274
|
const opener = new SessionOpener({ home: input.env.HOME ?? "" });
|
|
271
275
|
const dispatch = createSyncDispatcher({
|
|
272
276
|
open: request => opener.open(request),
|
|
@@ -289,9 +293,10 @@ export async function buildDaemon(input) {
|
|
|
289
293
|
const pushTransport = new RunnerTransport({
|
|
290
294
|
sync: async (reason, signal) => {
|
|
291
295
|
const acknowledged = closureAcks.take();
|
|
296
|
+
const suspendedJob = await suspendedPublication.named();
|
|
292
297
|
// A runner that takes no jobs right now (reconciling, or suspended for recovery) names no adapter.
|
|
293
298
|
try {
|
|
294
|
-
return await gateway.syncRunner({ adapterVersions: poller.accepting ? versions : {}, reason, acknowledgedClosureIds: acknowledged, guidancePolicyVersion: await guide.policyVersion(), roomActivityV1: true }, signal);
|
|
299
|
+
return await gateway.syncRunner({ adapterVersions: poller.accepting ? versions : {}, reason, acknowledgedClosureIds: acknowledged, guidancePolicyVersion: await guide.policyVersion(), roomActivityV1: true, ...(suspendedJob ? { suspendedJob } : {}) }, signal);
|
|
295
300
|
}
|
|
296
301
|
catch (error) {
|
|
297
302
|
closureAcks.restore(acknowledged);
|
|
@@ -307,7 +312,12 @@ export async function buildDaemon(input) {
|
|
|
307
312
|
await dispatch(result);
|
|
308
313
|
}
|
|
309
314
|
finally {
|
|
310
|
-
|
|
315
|
+
try {
|
|
316
|
+
await fixConsent.afterSync(result);
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
await suspendedPublication.afterSync(result);
|
|
320
|
+
}
|
|
311
321
|
}
|
|
312
322
|
},
|
|
313
323
|
openSocket: (target, events) => new ReverbSocket(target, events, { packageVersion: packageVersion() }),
|
|
@@ -328,7 +338,7 @@ export async function buildDaemon(input) {
|
|
|
328
338
|
throw new Error('Runner state requires authoritative reconciliation');
|
|
329
339
|
},
|
|
330
340
|
poller, sessionOpens: withGuide(guide, withAsks(asks, sessionOpens)),
|
|
331
|
-
reportRecoveryRequired: reason => { process.stderr.write(`${JSON.stringify({ event: 'runner_recovery_required', reason })}\n`); },
|
|
341
|
+
reportRecoveryRequired: reason => { suspendedPublication.markSuspended(); process.stderr.write(`${JSON.stringify({ event: 'runner_recovery_required', reason })}\n`); },
|
|
332
342
|
});
|
|
333
343
|
// Model tools have no Gateway relay. Each adapter owns and closes its job endpoint.
|
|
334
344
|
// The Claude host always closes; a failing Claude close never masks a Codex close failure.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const PUBLICATION_PHASES = ["publication_pending", "publication_terminal_pending"];
|
|
2
|
+
/** The job fence of a stored publication phase, or undefined for any other state. */
|
|
3
|
+
function heldPublication(state) {
|
|
4
|
+
if (!state || typeof state !== "object" || Array.isArray(state))
|
|
5
|
+
return undefined;
|
|
6
|
+
const { phase, jobId, generation } = state;
|
|
7
|
+
if (typeof phase !== "string" || !PUBLICATION_PHASES.includes(phase))
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof jobId !== "string" || !jobId.trim() || !Number.isSafeInteger(generation) || generation < 1)
|
|
10
|
+
return undefined;
|
|
11
|
+
return { jobId, generation: generation };
|
|
12
|
+
}
|
|
13
|
+
export class SuspendedPublication {
|
|
14
|
+
deps;
|
|
15
|
+
suspended = false;
|
|
16
|
+
cleared = false;
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.deps = deps;
|
|
19
|
+
}
|
|
20
|
+
/** The daemon suspended for recovery; from now on its held publication is named. */
|
|
21
|
+
markSuspended() { this.suspended = true; }
|
|
22
|
+
/** The job to name on the next sync. A state that cannot be read names nothing. */
|
|
23
|
+
async named() {
|
|
24
|
+
if (!this.suspended || this.cleared)
|
|
25
|
+
return undefined;
|
|
26
|
+
try {
|
|
27
|
+
return heldPublication(await this.deps.state.load());
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** After every sync: clears and restarts only when the answer closes the job the state still holds. */
|
|
34
|
+
async afterSync(result) {
|
|
35
|
+
const answer = result.suspendedJob;
|
|
36
|
+
if (!answer?.closed)
|
|
37
|
+
return;
|
|
38
|
+
const held = await this.named();
|
|
39
|
+
if (!held || held.jobId !== answer.jobId || held.generation !== answer.generation)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
await this.deps.state.clear();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
this.deps.log({ event: "runner_recovery_clear_failed", job_id: held.jobId, generation: held.generation });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.cleared = true;
|
|
49
|
+
this.deps.log({ event: "runner_recovery_cleared", job_id: held.jobId, generation: held.generation, gateway_status: answer.status });
|
|
50
|
+
this.deps.restart();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -19,6 +19,17 @@ export class GatewayToolError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
const ROOM_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
const SUSPENDED_JOB_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
23
|
+
/** A malformed answer is dropped, never a reason to refuse the sync: the runner then stays suspended. */
|
|
24
|
+
function parseSuspendedJob(value) {
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
26
|
+
return undefined;
|
|
27
|
+
const { job_id: jobId, generation, status, closed } = value;
|
|
28
|
+
if (typeof jobId !== "string" || !SUSPENDED_JOB_ID.test(jobId) || !Number.isSafeInteger(generation) || generation < 1
|
|
29
|
+
|| typeof status !== "string" || !/^[a-z_]{1,32}$/.test(status) || typeof closed !== "boolean")
|
|
30
|
+
return undefined;
|
|
31
|
+
return { jobId, generation: generation, status, closed };
|
|
32
|
+
}
|
|
22
33
|
const FIX_CONSENT_GRANT_ID = /^[a-zA-Z0-9-]{1,64}$/;
|
|
23
34
|
const FIX_CONSENT_REPOSITORY_KEY = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
24
35
|
const FIX_CONSENT_GRANTS_MAX = 20;
|
|
@@ -54,6 +65,7 @@ export function parseRunnerSync(result, runnerId) {
|
|
|
54
65
|
if (body === undefined || !Array.isArray(body.opens) || malformed("ask_deliveries") || malformed("closed_delivery_ids")) {
|
|
55
66
|
throw new GatewayToolError("Gateway returned an invalid runner sync result", "local_agent_runner_sync", "GATEWAY_INVALID_TOOL_RESULT");
|
|
56
67
|
}
|
|
68
|
+
const suspendedJob = parseSuspendedJob(body.suspended_job);
|
|
57
69
|
return {
|
|
58
70
|
opens: body.opens,
|
|
59
71
|
...(Array.isArray(body.ask_deliveries) ? { askDeliveries: body.ask_deliveries } : {}),
|
|
@@ -64,6 +76,7 @@ export function parseRunnerSync(result, runnerId) {
|
|
|
64
76
|
...(body.ask_alert === true ? { askAlert: true } : {}),
|
|
65
77
|
...(body.room_activity_enabled === true ? { roomActivityEnabled: true } : {}),
|
|
66
78
|
...(Array.isArray(body.fix_consent_grants) ? { fixConsentGrants: parseFixConsentGrants(body.fix_consent_grants) } : {}),
|
|
79
|
+
...(suspendedJob ? { suspendedJob } : {}),
|
|
67
80
|
jobAvailable: body.job_available === true,
|
|
68
81
|
push: parsePush(body.push, runnerId),
|
|
69
82
|
syncSeconds: clampSeconds(body.sync_seconds, 15, 120, 30),
|
|
@@ -608,6 +621,7 @@ export class McpGatewayClient {
|
|
|
608
621
|
...(acknowledged.length > 0 ? { acknowledged_closure_ids: acknowledged } : {}),
|
|
609
622
|
...(input.guidancePolicyVersion ? { guidance_policy_version: input.guidancePolicyVersion } : {}),
|
|
610
623
|
...(input.roomActivityV1 ? { room_activity_v1: true } : {}),
|
|
624
|
+
...(input.suspendedJob ? { suspended_job: { job_id: input.suspendedJob.jobId, generation: input.suspendedJob.generation } } : {}),
|
|
611
625
|
}, signal);
|
|
612
626
|
return parseRunnerSync(result, this.runnerId);
|
|
613
627
|
}
|
|
@@ -42,6 +42,8 @@ export function assertLocalFixConsent(value) {
|
|
|
42
42
|
if (!Array.isArray(modes) || !modes.length || new Set(modes).size !== modes.length || modes.some(m => m !== 'gateway_ci_v1' && m !== 'gateway_publish_v1'))
|
|
43
43
|
throw denied();
|
|
44
44
|
}
|
|
45
|
+
/** One fixed JSON line per repeated publication call; no payload, path or token. */
|
|
46
|
+
const reportRetry = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); };
|
|
45
47
|
/** Serves each job with the registration of the mode frozen in its own Fix scope. */
|
|
46
48
|
function multiModeHost(hosts) {
|
|
47
49
|
const verificationModes = hosts.map(h => { if (h.verificationMode === undefined)
|
|
@@ -94,7 +96,7 @@ export async function loadInstalledLocalFixHost(input) {
|
|
|
94
96
|
// The scope's own reviewed policy and this measured release make the registration the server CI host checks the job against.
|
|
95
97
|
const manifest = { verification_mode: mode, project: key, ci_profile: scope.ci_profile, ci_profile_fingerprint: protocolHash(scope.ci_profile), receipt: { ...measured.receipt } };
|
|
96
98
|
const registration = { ...manifest, review: { status: 'reviewed', fingerprint: protocolHash(manifest) } };
|
|
97
|
-
return createServerCiLocalFixHost({ runnerId: input.runnerId, privateRoot, git: config.git, registration, gateway: input.gateway, assertReleaseCurrent, canonicalApprovalId: async (publicationId) => {
|
|
99
|
+
return createServerCiLocalFixHost({ runnerId: input.runnerId, privateRoot, git: config.git, registration, gateway: input.gateway, assertReleaseCurrent, report: reportRetry, canonicalApprovalId: async (publicationId) => {
|
|
98
100
|
let approvals;
|
|
99
101
|
try {
|
|
100
102
|
approvals = await readPrivateJson(path.join(input.stateRoot, 'local-fix-pr-approvals.json'));
|
|
@@ -146,7 +148,7 @@ export async function loadInstalledLocalFixHost(input) {
|
|
|
146
148
|
const privateRoot = path.join(input.stateRoot, 'local-fix-publication'), s = await lstat(privateRoot);
|
|
147
149
|
if (!s.isDirectory() || s.isSymbolicLink() || (s.mode & 0o077) !== 0 || (process.getuid && s.uid !== process.getuid()) || await realpath(privateRoot) !== privateRoot)
|
|
148
150
|
throw denied();
|
|
149
|
-
const hosts = registrations.map(registration => createServerCiLocalFixHost({ runnerId: input.runnerId, privateRoot, git: config.git, registration, gateway: input.gateway, assertReleaseCurrent, canonicalApprovalId: async (publicationId) => {
|
|
151
|
+
const hosts = registrations.map(registration => createServerCiLocalFixHost({ runnerId: input.runnerId, privateRoot, git: config.git, registration, gateway: input.gateway, assertReleaseCurrent, report: reportRetry, canonicalApprovalId: async (publicationId) => {
|
|
150
152
|
let approvals;
|
|
151
153
|
try {
|
|
152
154
|
approvals = await readPrivateJson(path.join(input.stateRoot, 'local-fix-pr-approvals.json'));
|
|
@@ -14,6 +14,13 @@ import { GitSnapshotHost, SNAPSHOT_TREE_MAX_BYTES } from './git-snapshot.js';
|
|
|
14
14
|
import { PublicationOutcomeUnknownError } from './publisher.js';
|
|
15
15
|
import { isLocalFixAdapter } from './local-fix-adapters.js';
|
|
16
16
|
const denied = () => Error('LOCAL_FIX_SERVER_CI_REJECTED');
|
|
17
|
+
/** Work Decision b3d7eb22: a failed Gateway publication call is repeated for at most this long; the coordinator heartbeat keeps the lease. */
|
|
18
|
+
export const PUBLICATION_RETRY_WINDOW_MS = 900_000;
|
|
19
|
+
/** 5 s doubling to 130 s, longer than the Gateway's 120-s quiet window, so an absent working branch can be staged again (decision 1bddc563). */
|
|
20
|
+
export const publicationRetryDelay = (failures) => Math.min(5000 * 2 ** (failures - 1), 130_000);
|
|
21
|
+
/** Only the Gateway read of the fence failed; every local check passed, so the call may be repeated. */
|
|
22
|
+
class FenceUnreadable extends Error {
|
|
23
|
+
}
|
|
17
24
|
const safePath = (p) => typeof p === 'string' && p.length > 0 && !p.includes('\\') && !p.includes('\0') && !path.isAbsolute(p) && p.split('/').every(s => s !== '' && s !== '.' && s !== '..' && !/^(?:\.git|\.mcp(?:\..*)?|\.npmrc|\.netrc|id_rsa|id_ed25519|\.local-runner|\.runner|\.env(?:\..*)?|\.codex|\.claude|\.ssh|\.aws|\.config|auth\.(?:json|toml|ya?ml)(?:\..*)?|credentials(?:\..*)?|AGENTS\.md|CLAUDE\.md|mcp\.(?:json|toml|ya?ml)(?:\..*)?)$/i.test(s));
|
|
18
25
|
/** Gateway decision 8e7fff53: under any directory scope, test infrastructure, boot and dependency files stay as the base has them. */
|
|
19
26
|
const testInfrastructure = (p) => /(^|\/)(phpunit\.xml(\.dist)?|bitbucket-pipelines\.yml|composer\.[^/]+|package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml)$|^(scripts|app\/common)(\/|$)|(^|\/)(vendor|node_modules)(\/|$)/.test(p);
|
|
@@ -82,8 +89,22 @@ export function createServerCiLocalFixHost(options) {
|
|
|
82
89
|
throw denied();
|
|
83
90
|
let closed = false, settled = false, stageStarted = false, host, phase = 'validation_report', primary, disposing, finalization, seal;
|
|
84
91
|
const current = async () => { if (closed || !lifecycle.isCurrent() || !lifecycle.isFrozen())
|
|
85
|
-
throw denied(); await options.assertReleaseCurrent?.();
|
|
92
|
+
throw denied(); await options.assertReleaseCurrent?.(); let context; try {
|
|
93
|
+
context = (await options.gateway.localPublicationContext(fence)).context;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new FenceUnreadable();
|
|
97
|
+
} if (protocolHash(bindingProof(context)) !== protocolHash(binding) || closed || !lifecycle.isCurrent() || !lifecycle.isFrozen())
|
|
86
98
|
throw denied(); await seal?.assertUnchanged(); };
|
|
99
|
+
const readable = async () => { try {
|
|
100
|
+
await current();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof FenceUnreadable)
|
|
105
|
+
return false;
|
|
106
|
+
throw error;
|
|
107
|
+
} };
|
|
87
108
|
const dispose = () => disposing ??= (host?.dispose() ?? Promise.resolve());
|
|
88
109
|
return { close: async () => { closed = true; if (!finalization || settled) {
|
|
89
110
|
try {
|
|
@@ -198,21 +219,51 @@ export function createServerCiLocalFixHost(options) {
|
|
|
198
219
|
if (!security)
|
|
199
220
|
throw denied();
|
|
200
221
|
let result;
|
|
222
|
+
const sleep = options.sleep ?? delay, now = options.now ?? Date.now;
|
|
223
|
+
// Work Decision b3d7eb22: a string answer names a failed Gateway call, repeated with the same arguments while the
|
|
224
|
+
// fence holds. The streak ends at the first accepted answer; a delay that would outlast the window gives up as before.
|
|
225
|
+
const retrying = async (attempt) => {
|
|
226
|
+
let started;
|
|
227
|
+
for (let failures = 1;; failures++) {
|
|
228
|
+
const answer = await attempt();
|
|
229
|
+
if (typeof answer !== 'string')
|
|
230
|
+
return answer;
|
|
231
|
+
started ??= now();
|
|
232
|
+
const wait = publicationRetryDelay(failures);
|
|
233
|
+
if (now() - started + wait > PUBLICATION_RETRY_WINDOW_MS)
|
|
234
|
+
throw new PublicationOutcomeUnknownError();
|
|
235
|
+
try {
|
|
236
|
+
options.report?.({ event: 'local_fix_publication_retry', operation: answer, failures, delay_ms: wait });
|
|
237
|
+
}
|
|
238
|
+
catch { /* diagnostics only */ }
|
|
239
|
+
await sleep(wait);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
201
242
|
phase = 'stage';
|
|
202
243
|
stageStarted = true;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
244
|
+
let stages = 0;
|
|
245
|
+
const stageInput = { ...fence, operation: 'stage', files, ...(report ? { validationReport: report } : {}), securityReceipt: { ...security }, hostReceipt: { ...registration.receipt } };
|
|
246
|
+
// The same frozen files again: the Gateway reconciles a present branch or stages an absent one again (1bddc563).
|
|
247
|
+
result = await retrying(async () => {
|
|
248
|
+
if (stages++ > 0 && !await readable())
|
|
249
|
+
return 'fence';
|
|
250
|
+
let answer;
|
|
251
|
+
try {
|
|
252
|
+
answer = (await options.gateway.executeServerCiPublication(stageInput)).result;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return 'stage';
|
|
256
|
+
}
|
|
257
|
+
const id = answer.publication_id;
|
|
258
|
+
if (typeof id !== 'string' || !id)
|
|
259
|
+
throw new PublicationOutcomeUnknownError();
|
|
260
|
+
if (record.publication_id && record.publication_id !== id)
|
|
261
|
+
throw new PublicationOutcomeUnknownError();
|
|
262
|
+
record.publication_id = id;
|
|
263
|
+
await journal.save(key, record);
|
|
264
|
+
return answer.status === 'publication_unknown' ? 'stage' : answer;
|
|
265
|
+
});
|
|
266
|
+
const publicationId = String(result.publication_id);
|
|
216
267
|
const pending = new Set(['staged', 'validating', 'authorized', 'committing', 'committed', 'pushing', 'pushed', 'opening_pr']);
|
|
217
268
|
for (;;) {
|
|
218
269
|
phase = 'result';
|
|
@@ -221,7 +272,7 @@ export function createServerCiLocalFixHost(options) {
|
|
|
221
272
|
if (result.status === 'ci_failed')
|
|
222
273
|
throw new ServerCiPublicationError(result.ci?.error_code ?? result.failure?.code);
|
|
223
274
|
if (result.status === 'completed') {
|
|
224
|
-
await
|
|
275
|
+
await retrying(async () => await readable() ? {} : 'fence');
|
|
225
276
|
const ci = result.ci;
|
|
226
277
|
if (result.verification_mode !== mode || (mode === 'gateway_ci_v1' ? ci?.status !== 'passed' || ci.commit_sha !== result.commit_sha : ci?.status !== 'not_run' || result.verification?.status !== 'unverified') || typeof result.commit_sha !== 'string' || !/^[a-f0-9]{40}$/.test(result.commit_sha) || result.working_branch !== binding.working_branch)
|
|
227
278
|
throw new PublicationOutcomeUnknownError();
|
|
@@ -230,16 +281,20 @@ export function createServerCiLocalFixHost(options) {
|
|
|
230
281
|
if (result.waiting || !pending.has(String(result.status)))
|
|
231
282
|
throw new PublicationOutcomeUnknownError();
|
|
232
283
|
phase = 'advance';
|
|
233
|
-
await
|
|
234
|
-
await
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
284
|
+
await sleep(5000);
|
|
285
|
+
result = await retrying(async () => {
|
|
286
|
+
if (!await readable())
|
|
287
|
+
return 'fence';
|
|
288
|
+
const approval = await options.canonicalApprovalId?.(publicationId);
|
|
289
|
+
if (!await readable())
|
|
290
|
+
return 'fence';
|
|
291
|
+
try {
|
|
292
|
+
return (await options.gateway.executeServerCiPublication({ ...fence, operation: 'advance', publicationId, canonicalApprovalId: approval })).result;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return 'advance';
|
|
296
|
+
}
|
|
297
|
+
});
|
|
243
298
|
}
|
|
244
299
|
});
|
|
245
300
|
})().catch(error => {
|