@yemi33/minions 0.1.2381 → 0.1.2383
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/bin/minions.js +42 -2
- package/dashboard/js/refresh.js +8 -2
- package/dashboard/js/render-other.js +38 -0
- package/dashboard/js/render-work-items.js +61 -20
- package/dashboard/js/settings.js +7 -8
- package/dashboard/pages/engine.html +6 -0
- package/dashboard.js +110 -27
- package/docs/README.md +1 -0
- package/docs/claude-md-propagation.md +118 -0
- package/docs/completion-reports.md +20 -1
- package/docs/engine-restart.md +10 -0
- package/docs/runtime-adapters.md +54 -22
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +23 -0
- package/engine/acp-transport.js +63 -35
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +11 -4
- package/engine/cc-worker-pool.js +4 -2
- package/engine/claude-md-context.js +195 -0
- package/engine/cli.js +9 -1
- package/engine/comment-format.js +51 -14
- package/engine/consolidation.js +47 -3
- package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
- package/engine/dispatch.js +80 -23
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/lifecycle.js +195 -56
- package/engine/llm.js +59 -83
- package/engine/memory-store.js +3 -1
- package/engine/pipeline.js +147 -20
- package/engine/playbook.js +39 -6
- package/engine/pooled-agent-process.js +28 -26
- package/engine/prd-store.js +14 -6
- package/engine/quarantine-refs.js +33 -0
- package/engine/queries.js +8 -6
- package/engine/restart-health.js +69 -4
- package/engine/runtimes/claude.js +100 -25
- package/engine/runtimes/codex.js +19 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +85 -8
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +157 -64
- package/engine/spawn-agent.js +79 -113
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine/supervisor.js +72 -16
- package/engine/timeout.js +87 -16
- package/engine/untrusted-fence.js +2 -1
- package/engine.js +434 -125
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
package/engine/lifecycle.js
CHANGED
|
@@ -17,6 +17,7 @@ const harness = require('./harness');
|
|
|
17
17
|
const smallStateStore = require('./small-state-store');
|
|
18
18
|
const prdStore = require('./prd-store');
|
|
19
19
|
const { isBranchActive } = require('./cooldown');
|
|
20
|
+
const { resolveExecutionModel } = require('./execution-model');
|
|
20
21
|
const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
|
|
21
22
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
22
23
|
MINIONS_DIR, ENGINE_DIR, PLANS_DIR, INBOX_DIR, AGENTS_DIR } = queries;
|
|
@@ -2319,7 +2320,7 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
|
|
|
2319
2320
|
: null;
|
|
2320
2321
|
}
|
|
2321
2322
|
|
|
2322
|
-
async function updatePrAfterReview(agentId, pr, project, config, resultSummary, structuredCompletion = null, dispatchItem = null) {
|
|
2323
|
+
async function updatePrAfterReview(agentId, pr, project, config, resultSummary, structuredCompletion = null, dispatchItem = null, executionMetadata = null) {
|
|
2323
2324
|
|
|
2324
2325
|
if (!config) config = getConfig();
|
|
2325
2326
|
const completionStatus = normalizeCompletionStatus(structuredCompletion?.status);
|
|
@@ -2338,6 +2339,11 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2338
2339
|
const reviewProject = reviewContext.project;
|
|
2339
2340
|
const prScope = reviewContext.scope;
|
|
2340
2341
|
const reviewerName = config.agents?.[agentId]?.name || agentId;
|
|
2342
|
+
const reviewExecutionModel = resolveExecutionModel({
|
|
2343
|
+
reportedModel: executionMetadata?.reportedModel,
|
|
2344
|
+
capturedModel: dispatchItem?.executionModel,
|
|
2345
|
+
requestedModel: dispatchItem?.requestedModel,
|
|
2346
|
+
});
|
|
2341
2347
|
|
|
2342
2348
|
// Check actual review status from the platform (agent may have approved or requested changes)
|
|
2343
2349
|
// If platform hasn't propagated the vote yet (returns 'pending'), keep current status unchanged.
|
|
@@ -2549,6 +2555,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2549
2555
|
reviewer: reviewerName,
|
|
2550
2556
|
reviewedAt: ts(),
|
|
2551
2557
|
note: reviewNote,
|
|
2558
|
+
model: reviewExecutionModel.model,
|
|
2552
2559
|
dispatchId: dispatchItem?.id || structuredCompletion?.dispatchId || null,
|
|
2553
2560
|
sourceItem: dispatchItem?.meta?.item?.id || null,
|
|
2554
2561
|
...(reviewThreads.length > 0 ? { threads: reviewThreads } : {}),
|
|
@@ -3134,7 +3141,6 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3134
3141
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
3135
3142
|
options = { automationCauseKey: options, dispatchId: legacyDispatchId };
|
|
3136
3143
|
}
|
|
3137
|
-
const explicitlyChangedBranch = options.branchChanged !== false;
|
|
3138
3144
|
const prScope = project || 'central';
|
|
3139
3145
|
const automationCauseKey = options.automationCauseKey || options.dispatchItem?.meta?.automationCauseKey || '';
|
|
3140
3146
|
const fixDispatchId = options.dispatchItem?.id || options.dispatchId || legacyDispatchId || '';
|
|
@@ -3143,6 +3149,15 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3143
3149
|
source,
|
|
3144
3150
|
task: options.dispatchItem?.task,
|
|
3145
3151
|
});
|
|
3152
|
+
const evidenceOnlyReviewResolution = cause === shared.PR_FIX_CAUSE.REVIEW_FEEDBACK
|
|
3153
|
+
&& options.reviewFindingResolution;
|
|
3154
|
+
// Review-fix completion is governed by the live branch probe. Preserve the
|
|
3155
|
+
// legacy files_changed behavior for unrelated fix causes.
|
|
3156
|
+
let explicitlyChangedBranch = options.branchChanged !== false;
|
|
3157
|
+
if (cause === shared.PR_FIX_CAUSE.REVIEW_FEEDBACK && options.branchChange) {
|
|
3158
|
+
explicitlyChangedBranch = true;
|
|
3159
|
+
}
|
|
3160
|
+
if (evidenceOnlyReviewResolution) explicitlyChangedBranch = false;
|
|
3146
3161
|
let result = null;
|
|
3147
3162
|
shared.mutatePullRequests(prScope, (prs) => {
|
|
3148
3163
|
if (!Array.isArray(prs)) return prs;
|
|
@@ -4569,8 +4584,8 @@ function parseCompletionBoolean(value) {
|
|
|
4569
4584
|
}
|
|
4570
4585
|
|
|
4571
4586
|
// Detect a deliberate no-op completion — the agent correctly declined to make
|
|
4572
|
-
// changes (work was already shipped, dispatch premise was wrong,
|
|
4573
|
-
//
|
|
4587
|
+
// changes (work was already shipped, dispatch premise was wrong, or a finding
|
|
4588
|
+
// was proven resolved/invalid with current-code evidence) and should NOT be flagged as a
|
|
4574
4589
|
// silent failure for missing a PR. Honored signals:
|
|
4575
4590
|
// - completion.noop === true (canonical, primary)
|
|
4576
4591
|
// - completion.result === 'noop' OR completion.result.type === 'noop'
|
|
@@ -4578,8 +4593,8 @@ function parseCompletionBoolean(value) {
|
|
|
4578
4593
|
// detected, or null otherwise. The status must be a successful terminal state
|
|
4579
4594
|
// — a failed/partial/in-progress completion that also claims `noop` is a
|
|
4580
4595
|
// contradiction and falls through to the normal contract.
|
|
4581
|
-
function
|
|
4582
|
-
if (!completion || typeof completion !== 'object') return
|
|
4596
|
+
function completionDeclaresNoop(completion) {
|
|
4597
|
+
if (!completion || typeof completion !== 'object') return false;
|
|
4583
4598
|
const explicit = parseCompletionBoolean(completion.noop);
|
|
4584
4599
|
let resultIsNoop = false;
|
|
4585
4600
|
if (typeof completion.result === 'string') {
|
|
@@ -4587,7 +4602,11 @@ function parseCompletionNoop(completion) {
|
|
|
4587
4602
|
} else if (completion.result && typeof completion.result === 'object') {
|
|
4588
4603
|
resultIsNoop = String(completion.result.type || '').trim().toLowerCase() === 'noop';
|
|
4589
4604
|
}
|
|
4590
|
-
|
|
4605
|
+
return explicit === true || resultIsNoop;
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
function parseCompletionNoop(completion) {
|
|
4609
|
+
if (!completionDeclaresNoop(completion)) return null;
|
|
4591
4610
|
const status = normalizeCompletionStatus(completion.status);
|
|
4592
4611
|
if (status && status !== 'success' && status !== 'done' && status !== 'complete') return null;
|
|
4593
4612
|
const reason = String(
|
|
@@ -4600,6 +4619,86 @@ function parseCompletionNoop(completion) {
|
|
|
4600
4619
|
return reason || 'no-op completion';
|
|
4601
4620
|
}
|
|
4602
4621
|
|
|
4622
|
+
function classifyBenignZeroTargetOutcome(dispatchItem, completion, resultSummary) {
|
|
4623
|
+
const item = dispatchItem?.meta?.item;
|
|
4624
|
+
if (!item?._scheduleId || item.createdBy !== 'scheduler') return null;
|
|
4625
|
+
const type = dispatchItem?.type || item.type;
|
|
4626
|
+
if (![WORK_TYPE.SETUP, WORK_TYPE.EXPLORE, WORK_TYPE.REVIEW].includes(type)) return null;
|
|
4627
|
+
if (!completionDeclaresNoop(completion)) return null;
|
|
4628
|
+
if (parseCompletionBoolean(completion?.needs_rerun ?? completion?.needsRerun) === true) return null;
|
|
4629
|
+
if (parseCompletionBoolean(completion?.retryable) === true) return null;
|
|
4630
|
+
if (completion?.securityFlags?.injectionAttempt === true) return null;
|
|
4631
|
+
const failureClass = String(completion?.failure_class || '').trim().toLowerCase();
|
|
4632
|
+
const nonceUnavailable = failureClass === 'completion-nonce-unavailable'
|
|
4633
|
+
|| failureClass === 'nonce-unavailable';
|
|
4634
|
+
if (hasActionableFailureClass(failureClass) && !nonceUnavailable) return null;
|
|
4635
|
+
const status = normalizeCompletionStatus(completion?.status);
|
|
4636
|
+
const successfulStatus = status === 'success' || status === 'done' || status === 'complete';
|
|
4637
|
+
const nonceTransportFailure = nonceUnavailable && (status.startsWith('fail') || status === 'error');
|
|
4638
|
+
if (!successfulStatus && !nonceTransportFailure) return null;
|
|
4639
|
+
|
|
4640
|
+
const evidence = [...new Set([
|
|
4641
|
+
completion?.summary,
|
|
4642
|
+
completion?.noopReason,
|
|
4643
|
+
completion?.noop_reason,
|
|
4644
|
+
resultSummary,
|
|
4645
|
+
].filter(Boolean).map(value => String(value).trim()))].join(' ').trim();
|
|
4646
|
+
if (!evidence) return null;
|
|
4647
|
+
|
|
4648
|
+
const mutationOccurred = [
|
|
4649
|
+
/\b(?:a|one|[1-9]\d*)\s+(?:new\s+)?(?:work items?|reviews?|pull requests?|prs?|issues?)\s+(?:(?:was|were)\s+)?(?:created|enrolled|dispatched|posted|deleted|removed)\b/i,
|
|
4650
|
+
/\b(?:created|enrolled|dispatched|posted|deleted|removed)\s+[1-9]\d*\b/i,
|
|
4651
|
+
].some(pattern => pattern.test(evidence));
|
|
4652
|
+
if (mutationOccurred) return null;
|
|
4653
|
+
|
|
4654
|
+
const zeroTargets = [
|
|
4655
|
+
/\bno\s+[^.!?\n]{0,120}\b(?:needed|required|eligible|qualif(?:y|ied)|found|remain(?:ed|ing)?|to\s+(?:review|remove|dispatch|enroll|process|merge|fix))\b/i,
|
|
4656
|
+
/\bno\s+(?:open\s+issues?|eligible|qualifying|matching|context-only|merged|abandoned)\b/i,
|
|
4657
|
+
/\b(?:0|zero)\s+(?:eligible|qualifying|matching|context-only|open)\b/i,
|
|
4658
|
+
].some(pattern => pattern.test(evidence));
|
|
4659
|
+
if (!zeroTargets) return null;
|
|
4660
|
+
|
|
4661
|
+
const noMutation = [
|
|
4662
|
+
/\bno\s+(?:api\s+)?(?:post|mutations?|writes?|actions?|changes?|modifications?|updates?|delete requests?|work items?|review work items?)\s+(?:(?:was|were)\s+)?(?:made|performed|taken|sent|created)?\b/i,
|
|
4663
|
+
/\b(?:0|zero)\s+(?:were\s+)?(?:created|enrolled|dispatched|posted|deleted|removed)\b/i,
|
|
4664
|
+
/\b(?:created|enrolled|dispatched|posted|deleted|removed)\s+(?:0|zero)\b/i,
|
|
4665
|
+
].some(pattern => pattern.test(evidence));
|
|
4666
|
+
return noMutation ? evidence : null;
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4669
|
+
function validateReviewFixCompletion({ type, meta, branchChange, structuredCompletion } = {}) {
|
|
4670
|
+
if (!shared.isFixLikeWorkType(type)) return { valid: true };
|
|
4671
|
+
const cause = shared.getPrFixAutomationCause({
|
|
4672
|
+
dispatchKey: meta?.dispatchKey,
|
|
4673
|
+
source: meta?.source,
|
|
4674
|
+
task: meta?.item?.title,
|
|
4675
|
+
});
|
|
4676
|
+
if (cause !== shared.PR_FIX_CAUSE.REVIEW_FEEDBACK
|
|
4677
|
+
|| (branchChange?.changed === true && branchChange.evidence === 'remote-head')) {
|
|
4678
|
+
return { valid: true };
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
const findingId = shared.prReviewFindingIdentity(meta?.pr);
|
|
4682
|
+
const resolution = structuredCompletion?.reviewFindingResolution;
|
|
4683
|
+
const disposition = String(resolution?.disposition || '').trim().toLowerCase();
|
|
4684
|
+
const reportedFindingId = String(resolution?.findingId || '').trim();
|
|
4685
|
+
const currentCodeEvidence = String(resolution?.currentCodeEvidence || '').trim();
|
|
4686
|
+
const hasCurrentCodeReference = /(?:[a-z]:[\\/])?[\w./\\-]+\.[a-z0-9]+:\d+\b/i.test(currentCodeEvidence)
|
|
4687
|
+
|| /\bcommit\s+[0-9a-f]{7,40}\b/i.test(currentCodeEvidence);
|
|
4688
|
+
const validDisposition = disposition === 'already-resolved' || disposition === 'invalid';
|
|
4689
|
+
const explicitNoop = !!parseCompletionNoop(structuredCompletion);
|
|
4690
|
+
|
|
4691
|
+
if (explicitNoop && validDisposition && reportedFindingId === findingId && hasCurrentCodeReference) {
|
|
4692
|
+
return { valid: true, findingId, resolution };
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
return {
|
|
4696
|
+
valid: false,
|
|
4697
|
+
findingId,
|
|
4698
|
+
reason: `Review-fix completion did not advance the PR branch and must explicitly prove the exact finding is already resolved or invalid with current-code evidence. Expected reviewFindingResolution.findingId="${findingId}", disposition "already-resolved" or "invalid", and a file:line or commit reference. Shared platform ownership, viewerDidAuthor, or reviewer/fixer agent equality is not evidence.`,
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4603
4702
|
function normalizeReviewVerdict(verdict) {
|
|
4604
4703
|
const value = String(verdict || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
4605
4704
|
if (value === 'approve' || value === 'approved') return REVIEW_STATUS.APPROVED;
|
|
@@ -5482,11 +5581,15 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5482
5581
|
// P-d2a8f6c1: a nonce mismatch always counts as an agent-reported failure
|
|
5483
5582
|
// (regardless of what the discarded report claimed). This forces both the
|
|
5484
5583
|
// effectiveSuccess gate below and the dispatch result in engine.js to ERROR.
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5584
|
+
const benignNoop = nonceMismatch
|
|
5585
|
+
? null
|
|
5586
|
+
: classifyBenignZeroTargetOutcome(dispatchItem, structuredCompletion, completionGateSummary);
|
|
5587
|
+
const agentReportedFailure = !!nonceMismatch || (!benignNoop && (
|
|
5588
|
+
completionStatus.startsWith('fail')
|
|
5487
5589
|
|| completionStatus === 'error'
|
|
5488
5590
|
|| hasActionableFailureClass(structuredCompletion?.failure_class)
|
|
5489
|
-
|| agentNeedsRerun
|
|
5591
|
+
|| agentNeedsRerun
|
|
5592
|
+
));
|
|
5490
5593
|
// Untrusted completions cannot be honored as "retryable" by the agent — its
|
|
5491
5594
|
// retryable claim was discarded with the rest of the report. Force false.
|
|
5492
5595
|
const agentRetryable = nonceMismatch ? false : parseCompletionBoolean(structuredCompletion?.retryable);
|
|
@@ -5499,7 +5602,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5499
5602
|
if (autoRecovered) {
|
|
5500
5603
|
log('info', `Auto-recovery: agent failed but created ${prsCreatedCount} PR(s) — upgrading ${meta.item.id} to done`);
|
|
5501
5604
|
}
|
|
5502
|
-
const effectiveSuccess = !nonceMismatch && ((isSuccess && !agentReportedFailure) || autoRecovered);
|
|
5605
|
+
const effectiveSuccess = !nonceMismatch && (benignNoop || (isSuccess && !agentReportedFailure) || autoRecovered);
|
|
5503
5606
|
if (!nonceMismatch) {
|
|
5504
5607
|
const episodeOutcome = effectiveSuccess
|
|
5505
5608
|
? (autoRecovered || completionStatus.startsWith('partial') ? 'partial' : 'success')
|
|
@@ -5571,7 +5674,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5571
5674
|
// Without this, the second run produces no VERDICT, _retryCount increments,
|
|
5572
5675
|
// and after 3 such bailouts the WI flips to status=failed even though the
|
|
5573
5676
|
// original review was posted on the first run.
|
|
5574
|
-
if (effectiveSuccess && type === WORK_TYPE.REVIEW && meta?.item?.id) {
|
|
5677
|
+
if (effectiveSuccess && type === WORK_TYPE.REVIEW && meta?.item?.id && !benignNoop) {
|
|
5575
5678
|
const verdict = reviewVerdictFromCompletion(structuredCompletion) || parseReviewVerdict(resultSummary);
|
|
5576
5679
|
if (!verdict && isReviewBailout(resultSummary)) {
|
|
5577
5680
|
log('info', `Review ${meta.item.id} bailed out (review already posted) — treating as DONE without retry`);
|
|
@@ -5721,7 +5824,9 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5721
5824
|
|
|
5722
5825
|
let completionContractFailure = null;
|
|
5723
5826
|
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
|
|
5724
|
-
const nonTerminalCompletion =
|
|
5827
|
+
const nonTerminalCompletion = benignNoop
|
|
5828
|
+
? null
|
|
5829
|
+
: detectNonTerminalResultSummary(completionGateSummary, structuredCompletion, reportCompletion, { detectPhantom });
|
|
5725
5830
|
if (nonTerminalCompletion) {
|
|
5726
5831
|
const isPhantomDetection = nonTerminalCompletion.phrase === 'phantom-completion';
|
|
5727
5832
|
// P-e0b4f7a5 — before deferring a phantom retry, attempt to recover
|
|
@@ -5750,13 +5855,70 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5750
5855
|
}
|
|
5751
5856
|
|
|
5752
5857
|
// No-op signal: agent declared the work was correctly NOT done (already
|
|
5753
|
-
// shipped, dispatch premise wrong,
|
|
5858
|
+
// shipped, dispatch premise wrong, evidence-backed resolved finding, etc.). Skip the PR
|
|
5754
5859
|
// attachment contract — a missing PR is intentional, not a silent failure.
|
|
5755
|
-
|
|
5756
|
-
|
|
5860
|
+
let prFixBranchChange = null;
|
|
5861
|
+
let prFixBuildStillFailing = null;
|
|
5862
|
+
if (shared.isFixLikeWorkType(type) && effectiveSuccess && meta?.pr?.id) {
|
|
5863
|
+
try {
|
|
5864
|
+
prFixBranchChange = await detectPrFixBranchChange(meta, config);
|
|
5865
|
+
} catch (err) {
|
|
5866
|
+
log('warn', `PR fix no-op detection for ${meta.pr.id}: ${err.message}`);
|
|
5867
|
+
prFixBranchChange = { changed: null, reason: err.message };
|
|
5868
|
+
}
|
|
5869
|
+
if (prFixBranchChange?.changed === false) {
|
|
5870
|
+
const fixCause = shared.getPrFixAutomationCause({
|
|
5871
|
+
dispatchKey: dispatchItem?.meta?.dispatchKey,
|
|
5872
|
+
source: meta?.source,
|
|
5873
|
+
task: dispatchItem?.task,
|
|
5874
|
+
});
|
|
5875
|
+
if (fixCause === shared.PR_FIX_CAUSE.BUILD_FAILURE) {
|
|
5876
|
+
try {
|
|
5877
|
+
const liveStatus = await checkBuildStillFailingLive(meta.pr, meta.project);
|
|
5878
|
+
if (liveStatus === shared.BUILD_STATUS.FAILING) {
|
|
5879
|
+
prFixBuildStillFailing = liveStatus;
|
|
5880
|
+
}
|
|
5881
|
+
} catch (err) {
|
|
5882
|
+
log('warn', `Live build re-check for ${meta.pr.id}: ${err.message}`);
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
let noopRationale = (effectiveSuccess && !skipDoneStatus)
|
|
5888
|
+
? (parseCompletionNoop(structuredCompletion) || benignNoop)
|
|
5757
5889
|
: null;
|
|
5890
|
+
let reviewFixResolution = null;
|
|
5891
|
+
if (effectiveSuccess && !skipDoneStatus) {
|
|
5892
|
+
const reviewFixValidation = validateReviewFixCompletion({
|
|
5893
|
+
type,
|
|
5894
|
+
meta,
|
|
5895
|
+
branchChange: prFixBranchChange,
|
|
5896
|
+
structuredCompletion,
|
|
5897
|
+
});
|
|
5898
|
+
if (!reviewFixValidation.valid) {
|
|
5899
|
+
skipDoneStatus = true;
|
|
5900
|
+
meta._agentId = agentId;
|
|
5901
|
+
const detection = {
|
|
5902
|
+
phrase: 'review-fix-resolution-evidence-missing',
|
|
5903
|
+
reason: reviewFixValidation.reason,
|
|
5904
|
+
};
|
|
5905
|
+
const reason = meta?.item?.id
|
|
5906
|
+
? deferNonTerminalCompletion(meta, detection)
|
|
5907
|
+
: detection.reason;
|
|
5908
|
+
completionContractFailure = {
|
|
5909
|
+
reason,
|
|
5910
|
+
itemId: meta?.item?.id || dispatchItem.id,
|
|
5911
|
+
nonTerminal: true,
|
|
5912
|
+
processWorkItemFailure: false,
|
|
5913
|
+
};
|
|
5914
|
+
noopRationale = null;
|
|
5915
|
+
log('warn', `Review-fix completion rejected for ${meta?.item?.id || dispatchItem.id}: ${reviewFixValidation.reason}`);
|
|
5916
|
+
} else {
|
|
5917
|
+
reviewFixResolution = reviewFixValidation.resolution || null;
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5758
5920
|
if (noopRationale) {
|
|
5759
|
-
log('info', `No-op completion for ${meta
|
|
5921
|
+
log('info', `No-op completion for ${meta?.item?.id || dispatchItem.id}: ${noopRationale.slice(0, 200)}`);
|
|
5760
5922
|
}
|
|
5761
5923
|
|
|
5762
5924
|
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus && !noopRationale) {
|
|
@@ -5994,41 +6156,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5994
6156
|
|
|
5995
6157
|
// Archive is manual — user archives plans from the dashboard when ready
|
|
5996
6158
|
|
|
5997
|
-
let prFixBranchChange = null;
|
|
5998
|
-
let prFixBuildStillFailing = null;
|
|
5999
|
-
if (shared.isFixLikeWorkType(type) && effectiveSuccess && meta?.pr?.id) {
|
|
6000
|
-
try {
|
|
6001
|
-
prFixBranchChange = await detectPrFixBranchChange(meta, config);
|
|
6002
|
-
} catch (err) {
|
|
6003
|
-
log('warn', `PR fix no-op detection for ${meta.pr.id}: ${err.message}`);
|
|
6004
|
-
prFixBranchChange = { changed: null, reason: err.message };
|
|
6005
|
-
}
|
|
6006
|
-
// #639 — a BUILD_FAILURE fix that didn't change the branch is not
|
|
6007
|
-
// necessarily a genuine no-op: the agent may have found its own (or a
|
|
6008
|
-
// prior dispatch's) fix commit already present and concluded "nothing to
|
|
6009
|
-
// change" without checking whether CI actually turned green on that
|
|
6010
|
-
// commit. Re-poll live CI for the head before letting updatePrAfterFix
|
|
6011
|
-
// accept the no-op — if it's still failing, flag it so the no-op path
|
|
6012
|
-
// records a distinct fixIneffective signal instead of a clean no-op.
|
|
6013
|
-
if (prFixBranchChange?.changed === false) {
|
|
6014
|
-
const fixCause = shared.getPrFixAutomationCause({
|
|
6015
|
-
dispatchKey: dispatchItem?.meta?.dispatchKey,
|
|
6016
|
-
source: meta?.source,
|
|
6017
|
-
task: dispatchItem?.task,
|
|
6018
|
-
});
|
|
6019
|
-
if (fixCause === shared.PR_FIX_CAUSE.BUILD_FAILURE) {
|
|
6020
|
-
try {
|
|
6021
|
-
const liveStatus = await checkBuildStillFailingLive(meta.pr, meta.project);
|
|
6022
|
-
if (liveStatus === shared.BUILD_STATUS.FAILING) {
|
|
6023
|
-
prFixBuildStillFailing = liveStatus;
|
|
6024
|
-
}
|
|
6025
|
-
} catch (err) {
|
|
6026
|
-
log('warn', `Live build re-check for ${meta.pr.id}: ${err.message}`);
|
|
6027
|
-
}
|
|
6028
|
-
}
|
|
6029
|
-
}
|
|
6030
|
-
}
|
|
6031
|
-
|
|
6032
6159
|
// Scheduled task back-reference: update SQL state and write a linked inbox note.
|
|
6033
6160
|
if (meta?.item?._scheduleId) {
|
|
6034
6161
|
try {
|
|
@@ -6133,12 +6260,21 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6133
6260
|
const hardContractFail = completionContractFailure?.severity === 'hard'
|
|
6134
6261
|
|| completionContractFailure?.nonTerminal === true;
|
|
6135
6262
|
const finalResult = hardContractFail ? DISPATCH_RESULT.ERROR : (effectiveSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR);
|
|
6136
|
-
if (type === WORK_TYPE.REVIEW && finalResult === DISPATCH_RESULT.SUCCESS && !skipDoneStatus) {
|
|
6137
|
-
await updatePrAfterReview(
|
|
6263
|
+
if (type === WORK_TYPE.REVIEW && finalResult === DISPATCH_RESULT.SUCCESS && !skipDoneStatus && !benignNoop) {
|
|
6264
|
+
await updatePrAfterReview(
|
|
6265
|
+
agentId,
|
|
6266
|
+
meta?.pr,
|
|
6267
|
+
meta?.project,
|
|
6268
|
+
config,
|
|
6269
|
+
resultSummary,
|
|
6270
|
+
structuredCompletion,
|
|
6271
|
+
dispatchItem,
|
|
6272
|
+
{ reportedModel: model },
|
|
6273
|
+
);
|
|
6138
6274
|
} else if (type === WORK_TYPE.REVIEW) {
|
|
6139
6275
|
log('warn', `Skipping PR review metadata update for ${meta?.pr?.id || meta?.pr?.url || '(unknown PR)'} because review dispatch ${dispatchItem.id} did not complete cleanly`);
|
|
6140
6276
|
}
|
|
6141
|
-
if (shared.isFixLikeWorkType(type) && effectiveSuccess) {
|
|
6277
|
+
if (shared.isFixLikeWorkType(type) && effectiveSuccess && !skipDoneStatus) {
|
|
6142
6278
|
updatePrAfterFix(meta?.pr, meta?.project, meta?.source, {
|
|
6143
6279
|
branchChanged: fixCompletionChangedBranch(structuredCompletion),
|
|
6144
6280
|
automationCauseKey: meta?.automationCauseKey,
|
|
@@ -6147,6 +6283,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6147
6283
|
buildStillFailing: prFixBuildStillFailing,
|
|
6148
6284
|
config,
|
|
6149
6285
|
noopReason: noopRationale || meta?._noopReason || '',
|
|
6286
|
+
reviewFindingResolution: reviewFixResolution,
|
|
6150
6287
|
});
|
|
6151
6288
|
// W-mpg58wv3 — closure-loop dispatch. When the completed fix WI was spawned
|
|
6152
6289
|
// to address a minion REQUEST_CHANGES (its meta carries
|
|
@@ -6232,7 +6369,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6232
6369
|
const metricsResult = isAutoRetry ? 'retry' : finalResult;
|
|
6233
6370
|
updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
|
|
6234
6371
|
|
|
6235
|
-
return { resultSummary, taskUsage, autoRecovered, structuredCompletion, completionContractFailure, agentReportedFailure, agentRetryable, nonceMismatch };
|
|
6372
|
+
return { resultSummary, taskUsage, autoRecovered, structuredCompletion, completionContractFailure, agentReportedFailure, agentRetryable, nonceMismatch, benignNoop };
|
|
6236
6373
|
}
|
|
6237
6374
|
|
|
6238
6375
|
// ─── PR → PRD Status Sync ─────────────────────────────────────────────────────
|
|
@@ -6986,6 +7123,8 @@ module.exports = {
|
|
|
6986
7123
|
parseReviewVerdict,
|
|
6987
7124
|
isReviewBailout,
|
|
6988
7125
|
parseCompletionNoop,
|
|
7126
|
+
classifyBenignZeroTargetOutcome, // exported for testing
|
|
7127
|
+
validateReviewFixCompletion,
|
|
6989
7128
|
detectNonTerminalResultSummary,
|
|
6990
7129
|
deferNonTerminalCompletion,
|
|
6991
7130
|
deferPhantomCompletion,
|
package/engine/llm.js
CHANGED
|
@@ -18,7 +18,15 @@ const {
|
|
|
18
18
|
ENGINE_DEFAULTS,
|
|
19
19
|
resolveCcCli, resolveCcModel,
|
|
20
20
|
} = shared;
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
resolveRuntime,
|
|
23
|
+
resolveRuntimeByCapability,
|
|
24
|
+
listRuntimes,
|
|
25
|
+
RUNTIME_IMAGES_FILE_OPTION,
|
|
26
|
+
sanitizeInvocationOptions,
|
|
27
|
+
resolveInvocationOptions,
|
|
28
|
+
buildSpawnFlags,
|
|
29
|
+
} = require('./runtimes');
|
|
22
30
|
|
|
23
31
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
24
32
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
@@ -217,15 +225,17 @@ function _runtimeInstallHint(runtimeName, runtime) {
|
|
|
217
225
|
function _missingRuntimeMessage(runtimeName, runtime, reason) {
|
|
218
226
|
const selected = runtime?.name || runtimeName || 'configured';
|
|
219
227
|
if (!runtime) {
|
|
228
|
+
const installLines = listRuntimes().map((name) => {
|
|
229
|
+
const adapter = resolveRuntime(name);
|
|
230
|
+
return `- ${name}: ${_runtimeInstallHint(name, adapter)}`;
|
|
231
|
+
});
|
|
220
232
|
return [
|
|
221
233
|
`Minions can't run the configured runtime "${selected}".`,
|
|
222
234
|
'',
|
|
223
235
|
reason || 'The configured runtime is not registered.',
|
|
224
236
|
'',
|
|
225
237
|
'Choose a supported runtime in Settings -> Engine -> defaultCli/ccCli, then install its CLI:',
|
|
226
|
-
|
|
227
|
-
'- OpenAI Codex CLI: npm install -g @openai/codex or brew install --cask codex',
|
|
228
|
-
'- GitHub Copilot CLI: install via GitHub CLI/gh-copilot or download from https://github.com/github/copilot-cli/releases',
|
|
238
|
+
...installLines,
|
|
229
239
|
'',
|
|
230
240
|
'After installing, restart Minions so the dashboard and engine inherit the updated PATH.',
|
|
231
241
|
].join('\n');
|
|
@@ -239,10 +249,9 @@ function _missingRuntimeMessage(runtimeName, runtime, reason) {
|
|
|
239
249
|
'',
|
|
240
250
|
'After installing, restart Minions so the dashboard and engine inherit the updated PATH.',
|
|
241
251
|
];
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
lines.push('Or switch Settings -> Engine -> defaultCli/ccCli back to "claude" after installing Claude Code.');
|
|
252
|
+
const alternatives = listRuntimes().filter(name => name !== selected);
|
|
253
|
+
if (alternatives.length) {
|
|
254
|
+
lines.push(`Or switch Settings -> Engine -> defaultCli/ccCli to another installed runtime: ${alternatives.join(', ')}.`);
|
|
246
255
|
}
|
|
247
256
|
return lines.join('\n');
|
|
248
257
|
}
|
|
@@ -337,37 +346,12 @@ function _runtimeUnavailableResult(callOpts = {}) {
|
|
|
337
346
|
// ─── Spawn Helpers ───────────────────────────────────────────────────────────
|
|
338
347
|
|
|
339
348
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* the single source of truth and avoiding double-flag emission.
|
|
344
|
-
*
|
|
345
|
-
* Capability gating (matches engine.js _buildAgentSpawnFlags from P-2a6d9c4f):
|
|
346
|
-
* - effort/sessionId/maxBudget/bare/fallbackModel are dropped when the
|
|
347
|
-
* runtime's matching capability is false.
|
|
348
|
-
* - Copilot-specific opts (stream, disableBuiltinMcps,
|
|
349
|
-
* reasoningSummaries) are emitted unconditionally; the Claude adapter
|
|
350
|
-
* ignores them via the "tolerate unknown opts" rule.
|
|
349
|
+
* Encode the semantic invocation options through the stable runtime facade.
|
|
350
|
+
* Legacy named wrapper flags remain for compatibility; the opaque options bag
|
|
351
|
+
* is authoritative for adapter fields the wrapper does not know yet.
|
|
351
352
|
*/
|
|
352
353
|
function _buildSpawnAgentFlags(runtime, opts = {}) {
|
|
353
|
-
|
|
354
|
-
const flags = ['--runtime', String(runtime?.name || 'claude')];
|
|
355
|
-
|
|
356
|
-
if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
|
|
357
|
-
if (opts.model) flags.push('--model', String(opts.model));
|
|
358
|
-
if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
|
|
359
|
-
|
|
360
|
-
if (caps.effortLevels && opts.effort) flags.push('--effort', String(opts.effort));
|
|
361
|
-
if (caps.sessionResume && opts.sessionId) flags.push('--resume', String(opts.sessionId));
|
|
362
|
-
if (caps.budgetCap && opts.maxBudget != null) flags.push('--max-budget-usd', String(opts.maxBudget));
|
|
363
|
-
if (caps.bareMode && opts.bare === true) flags.push('--bare');
|
|
364
|
-
if (caps.fallbackModel && opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
|
|
365
|
-
|
|
366
|
-
if (opts.stream === 'on' || opts.stream === 'off') flags.push('--stream', opts.stream);
|
|
367
|
-
if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
|
|
368
|
-
if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
|
|
369
|
-
|
|
370
|
-
return flags;
|
|
354
|
+
return buildSpawnFlags(runtime, opts);
|
|
371
355
|
}
|
|
372
356
|
|
|
373
357
|
/**
|
|
@@ -379,15 +363,12 @@ function _buildSpawnAgentFlags(runtime, opts = {}) {
|
|
|
379
363
|
*
|
|
380
364
|
* Indirect path: uses engine/spawn-agent.js — mostly a fallback when the
|
|
381
365
|
* direct path can't resolve the binary cache. spawn-agent.js handles
|
|
382
|
-
* adapter resolution itself; we
|
|
383
|
-
*
|
|
366
|
+
* adapter resolution itself; we hand it the runtime plus an opaque options
|
|
367
|
+
* bag, so adding a harness flag does not require wrapper changes.
|
|
384
368
|
*/
|
|
385
369
|
function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
386
370
|
const {
|
|
387
|
-
direct, label, runtime,
|
|
388
|
-
maxBudget, bare, fallbackModel,
|
|
389
|
-
stream, disableBuiltinMcps, reasoningSummaries,
|
|
390
|
-
images,
|
|
371
|
+
direct, label, runtime, adapterOptions = {},
|
|
391
372
|
} = callOpts;
|
|
392
373
|
|
|
393
374
|
const id = uid();
|
|
@@ -399,24 +380,10 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
399
380
|
const cleanupFiles = [];
|
|
400
381
|
const cleanupDirs = [llmTmpDir];
|
|
401
382
|
const caps = (runtime && runtime.capabilities) || {};
|
|
402
|
-
const adapterOpts = {
|
|
403
|
-
|
|
404
|
-
maxBudget, bare, fallbackModel,
|
|
405
|
-
stream, disableBuiltinMcps, reasoningSummaries,
|
|
406
|
-
images,
|
|
383
|
+
const adapterOpts = sanitizeInvocationOptions(runtime, {
|
|
384
|
+
...adapterOptions,
|
|
407
385
|
tmpDir: llmTmpDir,
|
|
408
|
-
};
|
|
409
|
-
// Capability-gate per-flag opts before prompt construction so adapters can
|
|
410
|
-
// make resume-aware prompt decisions from the same opts used for argv.
|
|
411
|
-
if (!caps.effortLevels) adapterOpts.effort = undefined;
|
|
412
|
-
if (!caps.sessionResume) adapterOpts.sessionId = undefined;
|
|
413
|
-
if (!caps.budgetCap) adapterOpts.maxBudget = undefined;
|
|
414
|
-
if (!caps.bareMode) adapterOpts.bare = undefined;
|
|
415
|
-
if (!caps.fallbackModel) adapterOpts.fallbackModel = undefined;
|
|
416
|
-
// P-7b2e4d01 — defense in depth: never forward images to a runtime that
|
|
417
|
-
// can't read them (callLLM/callLLMStreaming already gate + surface a typed
|
|
418
|
-
// error upstream; this protects any other _spawnProcess caller).
|
|
419
|
-
if (!caps.imageInput) adapterOpts.images = undefined;
|
|
386
|
+
});
|
|
420
387
|
const finalPrompt = runtime.buildPrompt(promptText, sysPromptText, adapterOpts);
|
|
421
388
|
|
|
422
389
|
// ── Direct path ──
|
|
@@ -426,7 +393,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
426
393
|
// Only write a sys-prompt tmp file when the runtime actually consumes one
|
|
427
394
|
// via --system-prompt-file (Claude) AND we're not resuming (resumed sessions
|
|
428
395
|
// already have the sys prompt baked in).
|
|
429
|
-
if (!sessionId && sysPromptText && caps.systemPromptFile) {
|
|
396
|
+
if (!adapterOpts.sessionId && sysPromptText && caps.systemPromptFile) {
|
|
430
397
|
sysTmpPath = path.join(llmTmpDir, `direct-sys-${id}.md`);
|
|
431
398
|
fs.writeFileSync(sysTmpPath, sysPromptText);
|
|
432
399
|
cleanupFiles.push(sysTmpPath);
|
|
@@ -472,12 +439,16 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
472
439
|
const pidPath = promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
|
|
473
440
|
cleanupFiles.push(promptPath, sysPath, pidPath);
|
|
474
441
|
|
|
475
|
-
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
442
|
+
const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
443
|
+
const indirectAdapterOpts = { ...adapterOpts };
|
|
444
|
+
if (Array.isArray(indirectAdapterOpts.images) && indirectAdapterOpts.images.length) {
|
|
445
|
+
const imagesPath = path.join(llmTmpDir, `runtime-images-${id}.json`);
|
|
446
|
+
safeWrite(imagesPath, JSON.stringify(indirectAdapterOpts.images));
|
|
447
|
+
cleanupFiles.push(imagesPath);
|
|
448
|
+
indirectAdapterOpts[RUNTIME_IMAGES_FILE_OPTION] = imagesPath;
|
|
449
|
+
delete indirectAdapterOpts.images;
|
|
450
|
+
}
|
|
451
|
+
const adapterFlags = _buildSpawnAgentFlags(runtime, indirectAdapterOpts);
|
|
481
452
|
const args = [spawnScript, promptPath, sysPath, ...adapterFlags];
|
|
482
453
|
|
|
483
454
|
const proc = runFile(process.execPath, args, {
|
|
@@ -692,11 +663,16 @@ function _resolveModelForRuntime(runtime, callOpts) {
|
|
|
692
663
|
function _resolveRuntimeFeatureOpts({
|
|
693
664
|
stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
|
|
694
665
|
} = {}) {
|
|
695
|
-
const
|
|
666
|
+
const runtime = resolveRuntimeByCapability('acpWorkerPool');
|
|
667
|
+
const resolved = resolveInvocationOptions(runtime, {
|
|
668
|
+
stream,
|
|
669
|
+
disableBuiltinMcps,
|
|
670
|
+
reasoningSummaries,
|
|
671
|
+
}, { engineConfig: engineConfig || {} });
|
|
696
672
|
return {
|
|
697
|
-
stream: stream
|
|
698
|
-
disableBuiltinMcps: disableBuiltinMcps
|
|
699
|
-
reasoningSummaries: reasoningSummaries
|
|
673
|
+
stream: resolved.stream,
|
|
674
|
+
disableBuiltinMcps: resolved.disableBuiltinMcps,
|
|
675
|
+
reasoningSummaries: resolved.reasoningSummaries,
|
|
700
676
|
};
|
|
701
677
|
}
|
|
702
678
|
|
|
@@ -770,17 +746,17 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
770
746
|
const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
|
|
771
747
|
const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
|
|
772
748
|
if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
749
|
+
const runtimeOptions = resolveInvocationOptions(runtime, {
|
|
750
|
+
model, maxTurns, allowedTools, effort, sessionId,
|
|
751
|
+
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
752
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
753
|
+
}, { engineConfig: engineConfig || {} });
|
|
776
754
|
|
|
777
755
|
let _abort = null;
|
|
778
756
|
const promise = new Promise((resolve) => {
|
|
779
757
|
const _startMs = Date.now();
|
|
780
758
|
const { proc, cleanupFiles, cleanupDirs } = _spawnProcess(promptText, sysPromptText, {
|
|
781
|
-
direct, label, runtime,
|
|
782
|
-
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
783
|
-
...runtimeFeatureOpts,
|
|
759
|
+
direct, label, runtime, adapterOptions: runtimeOptions,
|
|
784
760
|
});
|
|
785
761
|
let resolved = false;
|
|
786
762
|
let exitFallbackTimer = null;
|
|
@@ -895,17 +871,17 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
895
871
|
const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
|
|
896
872
|
const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
|
|
897
873
|
if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
874
|
+
const runtimeOptions = resolveInvocationOptions(runtime, {
|
|
875
|
+
model, maxTurns, allowedTools, effort, sessionId,
|
|
876
|
+
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
877
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
878
|
+
}, { engineConfig: engineConfig || {} });
|
|
901
879
|
|
|
902
880
|
let _abort = null;
|
|
903
881
|
const promise = new Promise((resolve) => {
|
|
904
882
|
const _startMs = Date.now();
|
|
905
883
|
const { proc, cleanupFiles, cleanupDirs } = _spawnProcess(promptText, sysPromptText, {
|
|
906
|
-
direct, label, runtime,
|
|
907
|
-
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
908
|
-
...runtimeFeatureOpts,
|
|
884
|
+
direct, label, runtime, adapterOptions: runtimeOptions,
|
|
909
885
|
});
|
|
910
886
|
let resolved = false;
|
|
911
887
|
let exitFallbackTimer = null;
|
package/engine/memory-store.js
CHANGED
|
@@ -269,10 +269,12 @@ function searchMemoryRecords(query, options = {}) {
|
|
|
269
269
|
}
|
|
270
270
|
const limit = Math.max(1, Math.min(200, Number(options.limit) || 50));
|
|
271
271
|
args.push(limit);
|
|
272
|
+
// CROSS JOIN pins FTS as the outer loop; Node 22's SQLite otherwise prefers
|
|
273
|
+
// the scope index and evaluates MATCH once for every scoped record.
|
|
272
274
|
const sql = `
|
|
273
275
|
SELECT m.*, bm25(memory_records_fts, 5.0, 1.0, 2.0, 3.0) AS fts_rank
|
|
274
276
|
FROM memory_records_fts
|
|
275
|
-
JOIN memory_records m ON m.rowid=memory_records_fts.rowid
|
|
277
|
+
CROSS JOIN memory_records m ON m.rowid=memory_records_fts.rowid
|
|
276
278
|
WHERE ${where.join(' AND ')}
|
|
277
279
|
ORDER BY fts_rank
|
|
278
280
|
LIMIT ?
|