@edgehero/pi-dispatch-receiver 2.0.0 → 3.0.0
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/package.json +2 -2
- package/src/boot-retry.mjs +6 -3
- package/src/config.mjs +5 -5
- package/src/filter-azure.mjs +13 -0
- package/src/filter-forgejo.mjs +15 -0
- package/src/filter-gitlab.mjs +15 -0
- package/src/filter.mjs +16 -1
- package/src/poller.mjs +2 -1
- package/src/start.mjs +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgehero/pi-dispatch-receiver",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The pi-dispatch trigger edge: verifies GitHub, GitLab, Forgejo and Azure DevOps webhooks, or polls GitHub with no public URL, and queues one pi coding agent job per event (per replica, if set) for the worker.",
|
|
6
6
|
"keywords": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@octokit/webhooks": "14.2.0",
|
|
51
|
-
"@edgehero/pi-dispatch": "^
|
|
51
|
+
"@edgehero/pi-dispatch": "^3.0.0",
|
|
52
52
|
"bullmq": "5.80.4",
|
|
53
53
|
"ioredis": "5.11.1"
|
|
54
54
|
}
|
package/src/boot-retry.mjs
CHANGED
|
@@ -41,11 +41,14 @@
|
|
|
41
41
|
* deadline and was skipped -- a forge that came back at t=50 was missed, and "retries for the
|
|
42
42
|
* whole window" was false in the letter. The clamp is also what makes an uncancellable injected
|
|
43
43
|
* `sleep` safe: every sleep awaited here runs to completion, always followed by one more attempt.
|
|
44
|
-
* - Log lines carry `err?.message` only, never a value -- the `receiver_start_failed` posture.
|
|
44
|
+
* - Log lines carry `err?.message` only, never a value -- the `receiver_start_failed` posture. A GitHub
|
|
45
|
+
* answer adds its status and request id (`githubFailureFields`, issue #530), never a header beyond that.
|
|
45
46
|
*
|
|
46
47
|
* Total wall clock: the helper settles within `windowMs + attemptTimeoutMs`.
|
|
47
48
|
*/
|
|
48
49
|
|
|
50
|
+
import { githubFailureFields } from "@edgehero/pi-dispatch/octokit-log";
|
|
51
|
+
|
|
49
52
|
// The first gap is what `RestartSec=5` gave, now in-process: a blip recovers exactly as fast as the
|
|
50
53
|
// supervisor loop it replaces. It doubles to a cap borrowed from the worker's AUTH_RETRY_COOLDOWN_MS
|
|
51
54
|
// rationale (worker/src/start.mjs): short enough to pick the forge up within one gap of it coming
|
|
@@ -127,13 +130,13 @@ export async function retryIdentity(
|
|
|
127
130
|
if (err?.piDispatchConfig === true) throw err;
|
|
128
131
|
const remainingMs = deadline - now();
|
|
129
132
|
if (remainingMs <= 0) {
|
|
130
|
-
log({ event: "identity_retry_exhausted", forge, attempts: attempt, windowMs, reason: err?.message });
|
|
133
|
+
log({ event: "identity_retry_exhausted", forge, attempts: attempt, windowMs, reason: err?.message, ...githubFailureFields(err) });
|
|
131
134
|
throw err;
|
|
132
135
|
}
|
|
133
136
|
// Clamped, so the last gap ends exactly at the deadline and the final attempt runs at the
|
|
134
137
|
// window's edge -- see the header for the measured 35s-of-60s failure the clamp removes.
|
|
135
138
|
const gapMs = Math.min(delay, remainingMs);
|
|
136
|
-
log({ event: "identity_retry", forge, attempt, reason: err?.message, delayMs: gapMs });
|
|
139
|
+
log({ event: "identity_retry", forge, attempt, reason: err?.message, ...githubFailureFields(err), delayMs: gapMs });
|
|
137
140
|
await sleep(gapMs);
|
|
138
141
|
delay = Math.min(delay * 2, delayCapMs);
|
|
139
142
|
}
|
package/src/config.mjs
CHANGED
|
@@ -318,9 +318,9 @@ function loadTriggers(env, readFile, fileExists) {
|
|
|
318
318
|
}
|
|
319
319
|
const group = groups[run.kind];
|
|
320
320
|
if (on.type === "label") {
|
|
321
|
-
group.label.push({ index, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository });
|
|
321
|
+
group.label.push({ index, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository });
|
|
322
322
|
} else if (on.type === "comment") {
|
|
323
|
-
group.comment = { index, phrase: on.phrase, defaultFlow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository }; // parseTriggers guarantees at most one per forge
|
|
323
|
+
group.comment = { index, phrase: on.phrase, defaultFlow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository }; // parseTriggers guarantees at most one per forge
|
|
324
324
|
} else if (on.type === "pull_request") {
|
|
325
325
|
// The close-only split (issue #231): a close rule routes through the close gate (the CLOSER's
|
|
326
326
|
// resolved authority) and every other PR rule through the author gate, so they live in separate
|
|
@@ -329,17 +329,17 @@ function loadTriggers(env, readFile, fileExists) {
|
|
|
329
329
|
// split reads the SHARED close-word table, never a re-typed word.
|
|
330
330
|
const closeWord = PR_CLOSE_ACTIONS[run.kind];
|
|
331
331
|
if (closeWord !== undefined && on.action.includes(closeWord)) {
|
|
332
|
-
group.prClose.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
332
|
+
group.prClose.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
333
333
|
} else {
|
|
334
334
|
// `reviewStates` is null rather than an empty Set when unnarrowed: the filter tests it for
|
|
335
335
|
// presence, and an empty Set would read as "no verdict matches" and silently refuse everything.
|
|
336
|
-
group.pullRequest.push({ index, actions: new Set(on.action), reviewStates: on.reviewState ? new Set(on.reviewState) : null, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
336
|
+
group.pullRequest.push({ index, actions: new Set(on.action), reviewStates: on.reviewState ? new Set(on.reviewState) : null, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
337
337
|
}
|
|
338
338
|
} else if (on.type === "issue") {
|
|
339
339
|
// The close-trigger kind (issue #231). No predicate and no reviewStates by construction (the
|
|
340
340
|
// loader refused both), and `number`/`once` ride conditionally-present exactly as the loader
|
|
341
341
|
// normalized them -- the filter's close route reads them, nothing else does.
|
|
342
|
-
group.issue.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
342
|
+
group.issue.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
|
package/src/filter-azure.mjs
CHANGED
|
@@ -117,6 +117,16 @@ export function filterAzure(subset, triggers, knownFlows, selfId, authorized, de
|
|
|
117
117
|
...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
|
|
118
118
|
// #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
|
|
119
119
|
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
120
|
+
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
121
|
+
// that named only a model would have its provider's absence decide whether the model survives,
|
|
122
|
+
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
123
|
+
...(resolved.provider !== undefined ? { provider: resolved.provider } : {}),
|
|
124
|
+
...(resolved.model !== undefined ? { model: resolved.model } : {}),
|
|
125
|
+
...(resolved.maxTurns !== undefined ? { maxTurns: resolved.maxTurns } : {}),
|
|
126
|
+
...(resolved.models !== undefined ? { models: resolved.models } : {}),
|
|
127
|
+
// #501. Its own spread, the rule above: a dropped cap runs the job under the deployment's cap, or none,
|
|
128
|
+
// while the file reads as narrowed.
|
|
129
|
+
...(resolved.maxCostUsd !== undefined ? { maxCostUsd: resolved.maxCostUsd } : {}),
|
|
120
130
|
// The trigger's injected skills dir (REQ-PER-TRIGGER-SKILLS), at JOB level beside image/packages and
|
|
121
131
|
// NEVER inside `trigger`. That placement is sharpest here of all: `trigger` is carried into
|
|
122
132
|
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
|
|
@@ -194,6 +204,7 @@ function matchLabelRules(subset, triggers, labels, action) {
|
|
|
194
204
|
repository: rule.repository,
|
|
195
205
|
packages: rule.packages,
|
|
196
206
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
207
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
197
208
|
skillsDir: rule.skillsDir,
|
|
198
209
|
secrets: rule.secrets,
|
|
199
210
|
secretsProfile: rule.secretsProfile,
|
|
@@ -238,6 +249,7 @@ function routeComment(subset, triggers, knownFlows, targetType) {
|
|
|
238
249
|
repository: triggers.comment.repository,
|
|
239
250
|
packages: triggers.comment.packages,
|
|
240
251
|
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
|
|
252
|
+
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
241
253
|
skillsDir: triggers.comment.skillsDir,
|
|
242
254
|
secrets: triggers.comment.secrets,
|
|
243
255
|
secretsProfile: triggers.comment.secretsProfile,
|
|
@@ -271,6 +283,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
271
283
|
repository: rule.repository,
|
|
272
284
|
packages: rule.packages,
|
|
273
285
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
286
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
274
287
|
skillsDir: rule.skillsDir,
|
|
275
288
|
secrets: rule.secrets,
|
|
276
289
|
secretsProfile: rule.secretsProfile,
|
package/src/filter-forgejo.mjs
CHANGED
|
@@ -131,6 +131,16 @@ export function filterForgejo(eventName, subset, triggers, knownFlows, selfId, a
|
|
|
131
131
|
...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
|
|
132
132
|
// #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
|
|
133
133
|
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
134
|
+
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
135
|
+
// that named only a model would have its provider's absence decide whether the model survives,
|
|
136
|
+
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
137
|
+
...(resolved.provider !== undefined ? { provider: resolved.provider } : {}),
|
|
138
|
+
...(resolved.model !== undefined ? { model: resolved.model } : {}),
|
|
139
|
+
...(resolved.maxTurns !== undefined ? { maxTurns: resolved.maxTurns } : {}),
|
|
140
|
+
...(resolved.models !== undefined ? { models: resolved.models } : {}),
|
|
141
|
+
// #501. Its own spread, the rule above: a dropped cap runs the job under the deployment's cap, or none,
|
|
142
|
+
// while the file reads as narrowed.
|
|
143
|
+
...(resolved.maxCostUsd !== undefined ? { maxCostUsd: resolved.maxCostUsd } : {}),
|
|
134
144
|
// The trigger's injected skills dir (REQ-PER-TRIGGER-SKILLS), at JOB level beside image/packages and
|
|
135
145
|
// NEVER inside `trigger`. That placement is sharpest here of all: `trigger` is carried into
|
|
136
146
|
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
|
|
@@ -174,6 +184,7 @@ function routeIssueLabel(subset, triggers) {
|
|
|
174
184
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
175
185
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
176
186
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
187
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
177
188
|
skillsDir: rule.skillsDir,
|
|
178
189
|
secrets: rule.secrets,
|
|
179
190
|
secretsProfile: rule.secretsProfile,
|
|
@@ -223,6 +234,7 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
223
234
|
...(command !== undefined ? { command } : { flow }),
|
|
224
235
|
packages: triggers.comment.packages,
|
|
225
236
|
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
|
|
237
|
+
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
226
238
|
skillsDir: triggers.comment.skillsDir,
|
|
227
239
|
secrets: triggers.comment.secrets,
|
|
228
240
|
secretsProfile: triggers.comment.secretsProfile,
|
|
@@ -254,6 +266,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
254
266
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
255
267
|
packages: rule.packages,
|
|
256
268
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
269
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
257
270
|
skillsDir: rule.skillsDir,
|
|
258
271
|
secrets: rule.secrets,
|
|
259
272
|
secretsProfile: rule.secretsProfile,
|
|
@@ -309,6 +322,7 @@ function routeIssueClose(subset, triggers, authorized) {
|
|
|
309
322
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
310
323
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
311
324
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
325
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
312
326
|
skillsDir: rule.skillsDir,
|
|
313
327
|
secrets: rule.secrets,
|
|
314
328
|
secretsProfile: rule.secretsProfile,
|
|
@@ -345,6 +359,7 @@ function routePullRequestClose(subset, triggers, authorized) {
|
|
|
345
359
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
346
360
|
packages: rule.packages,
|
|
347
361
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
362
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
348
363
|
skillsDir: rule.skillsDir,
|
|
349
364
|
secrets: rule.secrets,
|
|
350
365
|
secretsProfile: rule.secretsProfile,
|
package/src/filter-gitlab.mjs
CHANGED
|
@@ -134,6 +134,16 @@ export function filterGitLab(subset, triggers, knownFlows, selfId, authorized, d
|
|
|
134
134
|
...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
|
|
135
135
|
// #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
|
|
136
136
|
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
137
|
+
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
138
|
+
// that named only a model would have its provider's absence decide whether the model survives,
|
|
139
|
+
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
140
|
+
...(resolved.provider !== undefined ? { provider: resolved.provider } : {}),
|
|
141
|
+
...(resolved.model !== undefined ? { model: resolved.model } : {}),
|
|
142
|
+
...(resolved.maxTurns !== undefined ? { maxTurns: resolved.maxTurns } : {}),
|
|
143
|
+
...(resolved.models !== undefined ? { models: resolved.models } : {}),
|
|
144
|
+
// #501. Its own spread, the rule above: a dropped cap runs the job under the deployment's cap, or none,
|
|
145
|
+
// while the file reads as narrowed.
|
|
146
|
+
...(resolved.maxCostUsd !== undefined ? { maxCostUsd: resolved.maxCostUsd } : {}),
|
|
137
147
|
// The trigger's injected skills dir (REQ-PER-TRIGGER-SKILLS), at JOB level beside image/packages and
|
|
138
148
|
// NEVER inside `trigger`. That placement is sharpest here of all: `trigger` is carried into
|
|
139
149
|
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
|
|
@@ -184,6 +194,7 @@ function routeLabel(subset, triggers, targetType) {
|
|
|
184
194
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
185
195
|
packages: rule.packages,
|
|
186
196
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
197
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
187
198
|
skillsDir: rule.skillsDir,
|
|
188
199
|
secrets: rule.secrets,
|
|
189
200
|
secretsProfile: rule.secretsProfile,
|
|
@@ -248,6 +259,7 @@ function mrResult(subset, rule, matched) {
|
|
|
248
259
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
249
260
|
packages: rule.packages,
|
|
250
261
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
262
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
251
263
|
skillsDir: rule.skillsDir,
|
|
252
264
|
secrets: rule.secrets,
|
|
253
265
|
secretsProfile: rule.secretsProfile,
|
|
@@ -294,6 +306,7 @@ function routeIssueClose(subset, triggers, authorized) {
|
|
|
294
306
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
295
307
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
296
308
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
309
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
297
310
|
skillsDir: rule.skillsDir,
|
|
298
311
|
secrets: rule.secrets,
|
|
299
312
|
secretsProfile: rule.secretsProfile,
|
|
@@ -328,6 +341,7 @@ function routeMergeRequestClose(subset, triggers, authorized) {
|
|
|
328
341
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
329
342
|
packages: rule.packages,
|
|
330
343
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
344
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
331
345
|
skillsDir: rule.skillsDir,
|
|
332
346
|
secrets: rule.secrets,
|
|
333
347
|
secretsProfile: rule.secretsProfile,
|
|
@@ -380,6 +394,7 @@ function routeNote(subset, triggers, knownFlows) {
|
|
|
380
394
|
...(command !== undefined ? { command } : { flow }),
|
|
381
395
|
packages: triggers.comment.packages,
|
|
382
396
|
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
|
|
397
|
+
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
383
398
|
skillsDir: triggers.comment.skillsDir,
|
|
384
399
|
secrets: triggers.comment.secrets,
|
|
385
400
|
secretsProfile: triggers.comment.secretsProfile,
|
package/src/filter.mjs
CHANGED
|
@@ -121,7 +121,8 @@ export function filter(eventName, subset, cfg, selfId, deliveryId, closerAuthori
|
|
|
121
121
|
if (!resolved.enqueue) return resolved; // carries the drop reason
|
|
122
122
|
|
|
123
123
|
// (3) Build the job from the INT-WEBHOOK-PAYLOAD-SUBSET fields only. No sender.login (not in the
|
|
124
|
-
// subset),
|
|
124
|
+
// subset), provider/model/maxTurns only when the matched rule named them (absent, the worker fills the
|
|
125
|
+
// deployment default, #502), no field outside the subset --
|
|
125
126
|
// with two trigger-context additions (issue #49): `matched` is harness-computed, the filter's own
|
|
126
127
|
// decision record naming the triggers.json entry that fired (not payload data at all); `comment`,
|
|
127
128
|
// present only on the comment route, carries the invoking comment's body/author_association, both
|
|
@@ -156,6 +157,16 @@ export function filter(eventName, subset, cfg, selfId, deliveryId, closerAuthori
|
|
|
156
157
|
// dropped exclusion runs the job WITH the tool, the destructive absence the loader's own sweep
|
|
157
158
|
// refuses a misspelling for, arriving through the plumbing instead of the spelling.
|
|
158
159
|
...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
|
|
160
|
+
// #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
|
|
161
|
+
// that named only a model would have its provider's absence decide whether the model survives,
|
|
162
|
+
// and a dropped model runs the job on the deployment default while the file reads as chosen.
|
|
163
|
+
...(resolved.provider !== undefined ? { provider: resolved.provider } : {}),
|
|
164
|
+
...(resolved.model !== undefined ? { model: resolved.model } : {}),
|
|
165
|
+
...(resolved.maxTurns !== undefined ? { maxTurns: resolved.maxTurns } : {}),
|
|
166
|
+
...(resolved.models !== undefined ? { models: resolved.models } : {}),
|
|
167
|
+
// #501. Its own spread, the rule above: a dropped cap runs the job under the deployment's cap, or none,
|
|
168
|
+
// while the file reads as narrowed.
|
|
169
|
+
...(resolved.maxCostUsd !== undefined ? { maxCostUsd: resolved.maxCostUsd } : {}),
|
|
159
170
|
// The trigger's injected skills dir (REQ-PER-TRIGGER-SKILLS), at JOB level beside image/packages and
|
|
160
171
|
// NEVER inside `trigger`. That placement is sharpest here of all: `trigger` is carried into
|
|
161
172
|
// /job/event.json, and a worker-host path in an agent-readable file is the leak prepare-local's
|
|
@@ -211,6 +222,7 @@ function routeIssueLabel(subset, triggers) {
|
|
|
211
222
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
212
223
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
213
224
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
225
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
214
226
|
skillsDir: rule.skillsDir,
|
|
215
227
|
secrets: rule.secrets,
|
|
216
228
|
secretsProfile: rule.secretsProfile,
|
|
@@ -271,6 +283,7 @@ function routeComment(subset, triggers, knownFlows) {
|
|
|
271
283
|
// <flow>` override changes WHICH flow runs, never which triggers.json entry authorized it.
|
|
272
284
|
packages: triggers.comment.packages,
|
|
273
285
|
image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
|
|
286
|
+
provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
|
|
274
287
|
skillsDir: triggers.comment.skillsDir,
|
|
275
288
|
secrets: triggers.comment.secrets,
|
|
276
289
|
secretsProfile: triggers.comment.secretsProfile,
|
|
@@ -369,6 +382,7 @@ function routePullRequest(subset, triggers, action) {
|
|
|
369
382
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
370
383
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
371
384
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
385
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
372
386
|
skillsDir: rule.skillsDir,
|
|
373
387
|
secrets: rule.secrets,
|
|
374
388
|
secretsProfile: rule.secretsProfile,
|
|
@@ -485,6 +499,7 @@ function routeClose(rules, number, closerAuthorized, matchedFor, targetFor) {
|
|
|
485
499
|
...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
|
|
486
500
|
packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
|
|
487
501
|
image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
|
|
502
|
+
provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
|
|
488
503
|
skillsDir: rule.skillsDir,
|
|
489
504
|
secrets: rule.secrets,
|
|
490
505
|
secretsProfile: rule.secretsProfile,
|
package/src/poller.mjs
CHANGED
|
@@ -203,7 +203,8 @@ export async function startPoller(env = process.env, deps = {}) {
|
|
|
203
203
|
// `auth ??= await ...` assigns only when the mint RESOLVES, so a retried getAuth re-invokes
|
|
204
204
|
// makeAuth. Caching the PROMISE instead would memoize the first failure and turn the retry loop
|
|
205
205
|
// into a rethrow spinner -- pinned by the poller's retry test.
|
|
206
|
-
|
|
206
|
+
// Issue #530: the GitHub client's own warnings go through this JSON log, never Octokit's plain console lines.
|
|
207
|
+
const getAuth = async () => (auth ??= await makeAuth(cfg.github, { log: (event, fields) => out({ event, ...fields }) }));
|
|
207
208
|
const selfId = await retryIdentity(
|
|
208
209
|
() => (selfIdFn ? selfIdFn(cfg.github) : getAuth().then((a) => a.selfId)),
|
|
209
210
|
{ forge: "github", windowMs: cfg.identityRetryWindowMs, log: out, now, sleep: sleepFn },
|
package/src/start.mjs
CHANGED
|
@@ -49,6 +49,7 @@ import { makeQueue } from "@edgehero/pi-dispatch/queue";
|
|
|
49
49
|
import { makeForgeRouter } from "./route.mjs";
|
|
50
50
|
import { judgeValkeyAtStart, parseConnection, valkeyClientContext } from "@edgehero/pi-dispatch/connection";
|
|
51
51
|
import { WATCH_DEBOUNCE_MS, changedWhileArming, makeWatchCloser, readBeforeArming } from "@edgehero/pi-dispatch/watch-closer";
|
|
52
|
+
import { githubFailureFields } from "@edgehero/pi-dispatch/octokit-log";
|
|
52
53
|
import { retryIdentity } from "./boot-retry.mjs";
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -145,7 +146,8 @@ export async function startReceiver(
|
|
|
145
146
|
// one credential decision -- an arm that resolved its identity is exactly the arm that can answer
|
|
146
147
|
// a permission question. This is also why the github handler's missing-resolver 503 is unreachable
|
|
147
148
|
// in a wired receiver: a boot that fails here mounts no `/` at all.
|
|
148
|
-
|
|
149
|
+
// Issue #530: the GitHub client's own warnings go through this JSON log, never Octokit's plain console lines.
|
|
150
|
+
const auth = await retryIdentity(() => makeAuth(cfg.github, { log: (event, fields) => log({ event, ...fields }) }), { forge: "github", ...retryOpts });
|
|
149
151
|
selfId = auth.selfId;
|
|
150
152
|
log({ event: "self_identity", id: selfId, source: cfg.github.source });
|
|
151
153
|
// The lookup token asks the mint to narrow to metadata:read -- the App path honors it GitHub-side,
|
|
@@ -372,7 +374,7 @@ if (isEntryModule(import.meta.url)) {
|
|
|
372
374
|
// An unhandled rejection is printed as its message alone (PR #475's review), never Node's print of the whole reason.
|
|
373
375
|
installRejectionPrinter();
|
|
374
376
|
startReceiver(process.env).catch((err) => {
|
|
375
|
-
process.stderr.write(`${JSON.stringify({ event: "receiver_start_failed", reason: err?.message })}\n`);
|
|
377
|
+
process.stderr.write(`${JSON.stringify({ event: "receiver_start_failed", reason: err?.message, ...githubFailureFields(err) })}\n`);
|
|
376
378
|
// entryExitCode, NOT a bare 1. This file is what `receiver.service` execs -- cli.mjs is not on that
|
|
377
379
|
// path -- so the mapping cli.mjs documents ("a supervisor restarting on exit 2 would loop on a config
|
|
378
380
|
// that can never parse") only reaches a real deployment from here. A tagged config refusal exits 2
|