neoagent 2.5.2-beta.19 → 2.5.2-beta.20
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 +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/services/ai/loop/conversation_loop.js +39 -91
- package/server/services/ai/loop/error_recovery.js +4 -15
- package/server/services/ai/loop/messaging_delivery.js +0 -35
- package/server/services/ai/loop/progress_classification.js +13 -0
- package/server/services/ai/loop/progress_monitor.js +3 -3
- package/server/services/ai/repetitionGuard.js +47 -2
- package/server/services/ai/taskAnalysis.js +4 -0
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
efe2ad6d1aff0358de22202a239e2829
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "2093031085" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -134794,7 +134794,7 @@ r===$&&A.b()
|
|
|
134794
134794
|
p.push(A.jP(q,A.j9(!1,new A.a_(B.uG,A.d8(new A.cA(B.jt,new A.a7N(r,q),q),q,q),q),!1,B.H,!0),q,q,0,0,0,q))}r=!1
|
|
134795
134795
|
if(!s.ay)if(!s.ch){r=s.e
|
|
134796
134796
|
r===$&&A.b()
|
|
134797
|
-
r=B.b.u("
|
|
134797
|
+
r=B.b.u("mqgmwb41-5bb5db5").length!==0&&r.b}if(r){r=s.d
|
|
134798
134798
|
r===$&&A.b()
|
|
134799
134799
|
r=r.aP&&!r.ai?84:0
|
|
134800
134800
|
s=s.e
|
|
@@ -140506,7 +140506,7 @@ $S:0}
|
|
|
140506
140506
|
A.a_6.prototype={}
|
|
140507
140507
|
A.SQ.prototype={
|
|
140508
140508
|
nb(a){var s=this
|
|
140509
|
-
if(B.b.u("
|
|
140509
|
+
if(B.b.u("mqgmwb41-5bb5db5").length===0||s.a!=null)return
|
|
140510
140510
|
s.AU()
|
|
140511
140511
|
s.a=A.on(B.RH,new A.bc8(s))},
|
|
140512
140512
|
AU(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
|
|
@@ -140524,7 +140524,7 @@ if(!t.f.b(k)){s=1
|
|
|
140524
140524
|
break}i=J.a3(k,"buildId")
|
|
140525
140525
|
h=i==null?null:B.b.u(J.p(i))
|
|
140526
140526
|
j=h==null?"":h
|
|
140527
|
-
if(J.bi(j)===0||J.d(j,"
|
|
140527
|
+
if(J.bi(j)===0||J.d(j,"mqgmwb41-5bb5db5")){s=1
|
|
140528
140528
|
break}n.b=!0
|
|
140529
140529
|
n.F()
|
|
140530
140530
|
p=2
|
|
@@ -140541,7 +140541,7 @@ case 2:return A.i(o.at(-1),r)}})
|
|
|
140541
140541
|
return A.k($async$AU,r)},
|
|
140542
140542
|
vE(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
|
|
140543
140543
|
var $async$vE=A.h(function(a2,a3){if(a2===1){o.push(a3)
|
|
140544
|
-
s=p}for(;;)switch(s){case 0:if(B.b.u("
|
|
140544
|
+
s=p}for(;;)switch(s){case 0:if(B.b.u("mqgmwb41-5bb5db5").length===0||n.c){s=1
|
|
140545
140545
|
break}n.c=!0
|
|
140546
140546
|
n.F()
|
|
140547
140547
|
p=4
|
|
@@ -61,7 +61,6 @@ const {
|
|
|
61
61
|
shouldContinueAfterBlankToolFailure,
|
|
62
62
|
} = require('./blank_recovery');
|
|
63
63
|
const {
|
|
64
|
-
shouldRetryMessagingRun,
|
|
65
64
|
shouldSendMessagingErrorFallback,
|
|
66
65
|
} = require('./error_recovery');
|
|
67
66
|
const {
|
|
@@ -143,7 +142,6 @@ const {
|
|
|
143
142
|
summarizeToolExecutions,
|
|
144
143
|
summarizeAvailableTools,
|
|
145
144
|
inferToolFailureMessage,
|
|
146
|
-
buildAutonomousRecoveryContext,
|
|
147
145
|
} = require('../toolEvidence');
|
|
148
146
|
const {
|
|
149
147
|
buildMemoryConsolidationInstructions,
|
|
@@ -769,14 +767,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
769
767
|
let iteration = 0;
|
|
770
768
|
let totalTokens = 0;
|
|
771
769
|
let lastContent = '';
|
|
772
|
-
let stepIndex =
|
|
773
|
-
if (!Number.isFinite(stepIndex) || stepIndex < 0) {
|
|
774
|
-
stepIndex = 0;
|
|
775
|
-
}
|
|
776
|
-
if (options.messagingAutonomousRetryCount > 0) {
|
|
777
|
-
const existingStep = db.prepare('SELECT COALESCE(MAX(step_index), 0) AS maxStep FROM agent_steps WHERE run_id = ?').get(runId);
|
|
778
|
-
stepIndex = Math.max(stepIndex, Number(existingStep?.maxStep || 0));
|
|
779
|
-
}
|
|
770
|
+
let stepIndex = 0;
|
|
780
771
|
let failedStepCount = 0;
|
|
781
772
|
let modelFailureRecoveries = 0;
|
|
782
773
|
let promptMetrics = {};
|
|
@@ -1084,7 +1075,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1084
1075
|
const urgency = readOnlyCount >= 6 ? 'CRITICAL' : 'ACTION REQUIRED';
|
|
1085
1076
|
messages.push({
|
|
1086
1077
|
role: 'system',
|
|
1087
|
-
content: `${urgency} — ${readOnlyCount} consecutive read-only turns: You have been gathering information for ${readOnlyCount} turns without writing, creating,
|
|
1078
|
+
content: `${urgency} — ${readOnlyCount} consecutive read-only turns: You have been gathering information for ${readOnlyCount} turns without implementation progress such as writing, editing, creating a branch, running verification, or delivering a final/blocker. Switch method now: establish or reuse a writable checkout, create a task branch, edit files, run verification, open/update a PR, or call task_complete with the real blocker. If the user has been waiting, send one concise interim update, but that does not satisfy the method switch. Do not do more remote tree/list/content scraping first. Do not assume tool results exist at /tmp paths unless a tool explicitly returned that readable path.`,
|
|
1088
1079
|
});
|
|
1089
1080
|
}
|
|
1090
1081
|
}
|
|
@@ -1911,12 +1902,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1911
1902
|
reason: recoveryDecision.decision.reason,
|
|
1912
1903
|
}, { agentId });
|
|
1913
1904
|
if (recoveryDecision.decision.status === 'continue') {
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1905
|
+
engine.recordRunEvent(userId, runId, 'blank_reply_recovery_blocker_used', {
|
|
1906
|
+
reason: recoveryDecision.decision.reason,
|
|
1907
|
+
stepIndex,
|
|
1908
|
+
failedStepCount,
|
|
1909
|
+
}, { agentId });
|
|
1910
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1911
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1912
|
+
if (conversationId) {
|
|
1913
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1914
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
1915
|
+
}
|
|
1916
|
+
} else {
|
|
1917
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1918
|
+
if (conversationId) {
|
|
1919
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1920
|
+
.run(conversationId, 'assistant', lastContent, recoveredTokens);
|
|
1921
|
+
}
|
|
1920
1922
|
}
|
|
1921
1923
|
}
|
|
1922
1924
|
}
|
|
@@ -1944,10 +1946,28 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1944
1946
|
stepIndex,
|
|
1945
1947
|
failedStepCount,
|
|
1946
1948
|
}, { agentId });
|
|
1947
|
-
|
|
1949
|
+
if (triggerSource === 'messaging') {
|
|
1950
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1951
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1952
|
+
if (conversationId) {
|
|
1953
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1954
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
1955
|
+
}
|
|
1956
|
+
} else {
|
|
1957
|
+
throw new Error(`Iteration budget exhausted before judged completion after ${maxIterations} iterations.`);
|
|
1958
|
+
}
|
|
1948
1959
|
}
|
|
1949
1960
|
if (stepIndex > 0 && !lastToolWasMessaging && iteration < maxIterations) {
|
|
1950
|
-
|
|
1961
|
+
if (triggerSource === 'messaging') {
|
|
1962
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1963
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1964
|
+
if (conversationId) {
|
|
1965
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1966
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
1967
|
+
}
|
|
1968
|
+
} else {
|
|
1969
|
+
throw new Error('Run ended without an explicit completion or blocker reply.');
|
|
1970
|
+
}
|
|
1951
1971
|
}
|
|
1952
1972
|
}
|
|
1953
1973
|
|
|
@@ -2190,78 +2210,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2190
2210
|
}
|
|
2191
2211
|
|
|
2192
2212
|
const runMeta = engine.activeRuns.get(runId);
|
|
2193
|
-
const retryCount = Number(options.messagingAutonomousRetryCount || 0);
|
|
2194
|
-
// Rate-limit errors (429) must not trigger messaging retries: the model
|
|
2195
|
-
// won't be available in the milliseconds between retries, so spawning new
|
|
2196
|
-
// runs just compounds the rate-limit pressure with no benefit.
|
|
2197
|
-
const canRetryMessagingRun = shouldRetryMessagingRun({
|
|
2198
|
-
triggerSource,
|
|
2199
|
-
options,
|
|
2200
|
-
runMeta,
|
|
2201
|
-
error: err,
|
|
2202
|
-
retryCount,
|
|
2203
|
-
retryLimit: engine.getMessagingRetryLimit(maxIterations),
|
|
2204
|
-
});
|
|
2205
|
-
|
|
2206
|
-
if (canRetryMessagingRun) {
|
|
2207
|
-
const recoveryContext = buildAutonomousRecoveryContext({
|
|
2208
|
-
err,
|
|
2209
|
-
toolExecutions,
|
|
2210
|
-
tools,
|
|
2211
|
-
userMessage,
|
|
2212
|
-
visibleMessageSent: Boolean(
|
|
2213
|
-
runMeta?.lastSentMessage
|
|
2214
|
-
|| runMeta?.lastInterimMessage
|
|
2215
|
-
|| runMeta?.messagingSent === true
|
|
2216
|
-
),
|
|
2217
|
-
});
|
|
2218
|
-
db.prepare('UPDATE agent_runs SET status = ?, error = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
2219
|
-
.run('retrying', err.message, runId);
|
|
2220
|
-
console.warn(
|
|
2221
|
-
`[Run ${shortenRunId(runId)}] retrying_messaging_attempt=${retryCount + 1} reason=${summarizeForLog(err.message, 140)}`
|
|
2222
|
-
);
|
|
2223
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2224
|
-
engine.stopMessagingProgressSupervisor(runId);
|
|
2225
|
-
engine.activeRuns.delete(runId);
|
|
2226
|
-
engine.emit(userId, 'run:interim', {
|
|
2227
|
-
runId,
|
|
2228
|
-
message: 'Retrying internally after a transient failure.',
|
|
2229
|
-
phase: 'retrying'
|
|
2230
|
-
});
|
|
2231
|
-
|
|
2232
|
-
const retryOptions = {
|
|
2233
|
-
...options,
|
|
2234
|
-
runId,
|
|
2235
|
-
messagingRetryStepOffset: stepIndex,
|
|
2236
|
-
messagingAutonomousRetryCount: retryCount + 1,
|
|
2237
|
-
messagingRetryState: {
|
|
2238
|
-
lastFinalMessage: String(runMeta?.lastSentMessage || options?.messagingRetryState?.lastFinalMessage || '').trim(),
|
|
2239
|
-
explicitMessageSent: runMeta?.explicitMessageSent === true || options?.messagingRetryState?.explicitMessageSent === true,
|
|
2240
|
-
interimHistory: cloneInterimHistory([
|
|
2241
|
-
...(Array.isArray(options?.messagingRetryState?.interimHistory) ? options.messagingRetryState.interimHistory : []),
|
|
2242
|
-
...(Array.isArray(runMeta?.interimMessages) ? runMeta.interimMessages : []),
|
|
2243
|
-
]),
|
|
2244
|
-
goalContract: mergeGoalContracts(
|
|
2245
|
-
options?.messagingRetryState?.goalContract || null,
|
|
2246
|
-
runMeta?.goalContract || null,
|
|
2247
|
-
),
|
|
2248
|
-
lastUserVisibleUpdateAt: runMeta?.progressLedger?.lastUserVisibleUpdateAt || options?.messagingRetryState?.lastUserVisibleUpdateAt || null,
|
|
2249
|
-
lastFinalDeliveryAt: runMeta?.progressLedger?.lastFinalDeliveryAt || options?.messagingRetryState?.lastFinalDeliveryAt || null,
|
|
2250
|
-
heartbeatCount: Number(runMeta?.progressLedger?.heartbeatCount || options?.messagingRetryState?.heartbeatCount || 0),
|
|
2251
|
-
progressState: runMeta?.progressLedger?.progressState || options?.messagingRetryState?.progressState || 'active',
|
|
2252
|
-
lastVerifiedProgressAt: runMeta?.progressLedger?.lastVerifiedProgressAt || options?.messagingRetryState?.lastVerifiedProgressAt || null,
|
|
2253
|
-
},
|
|
2254
|
-
context: {
|
|
2255
|
-
...(options.context || {}),
|
|
2256
|
-
additionalContext: [
|
|
2257
|
-
options.context?.additionalContext || '',
|
|
2258
|
-
recoveryContext,
|
|
2259
|
-
].filter(Boolean).join('\n\n')
|
|
2260
|
-
}
|
|
2261
|
-
};
|
|
2262
|
-
|
|
2263
|
-
return engine.runWithModel(userId, userMessage, retryOptions, _modelOverride);
|
|
2264
|
-
}
|
|
2265
2213
|
|
|
2266
2214
|
const deliverableFailureResponse = err?.deliverableResult?.summary
|
|
2267
2215
|
|| err?.deliverableValidation?.summary
|
|
@@ -13,21 +13,10 @@ function isRateLimitError(error = null) {
|
|
|
13
13
|
return /429|rate.?limit|free-models-per/i.test(String(error?.message || ''));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function shouldRetryMessagingRun({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
error = null,
|
|
21
|
-
retryCount = 0,
|
|
22
|
-
retryLimit = 0,
|
|
23
|
-
} = {}) {
|
|
24
|
-
return triggerSource === 'messaging'
|
|
25
|
-
&& Boolean(options.source)
|
|
26
|
-
&& Boolean(options.chatId)
|
|
27
|
-
&& !hasTerminalMessagingDelivery(runMeta)
|
|
28
|
-
&& error?.disableAutonomousRetry !== true
|
|
29
|
-
&& !isRateLimitError(error)
|
|
30
|
-
&& Number(retryCount || 0) < Number(retryLimit || 0);
|
|
16
|
+
function shouldRetryMessagingRun() {
|
|
17
|
+
// Messaging recovery must stay inside the current run. Re-entering
|
|
18
|
+
// runWithModel starts over from the original task and repeats tool work.
|
|
19
|
+
return false;
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
function shouldSendMessagingErrorFallback({
|
|
@@ -16,9 +16,6 @@ const {
|
|
|
16
16
|
evaluateProgressLiveness,
|
|
17
17
|
} = require('./progress_monitor');
|
|
18
18
|
|
|
19
|
-
// Force a visible WhatsApp status message after this long with no user-visible update
|
|
20
|
-
const FORCE_VISIBLE_UPDATE_MS = 2 * 60 * 1000;
|
|
21
|
-
|
|
22
19
|
function isoNow() {
|
|
23
20
|
return new Date().toISOString();
|
|
24
21
|
}
|
|
@@ -56,39 +53,7 @@ async function sendRuntimeMessagingHeartbeat(engine, runId, options = {}) {
|
|
|
56
53
|
runMeta.lastSupervisorNudgeAt = createdAt;
|
|
57
54
|
engine.updateRunProgress(runId, { heartbeatCount });
|
|
58
55
|
|
|
59
|
-
// If the user hasn't seen any update for FORCE_VISIBLE_UPDATE_MS, push one directly
|
|
60
|
-
// rather than just queuing another invisible steering message the AI may ignore.
|
|
61
56
|
const ledger = runMeta.progressLedger || {};
|
|
62
|
-
const lastVisibleMs = timestampMs(ledger.lastUserVisibleUpdateAt, 0);
|
|
63
|
-
const startedMs = timestampMs(runMeta.startedAtIso, 0) || (Date.now() - 60000);
|
|
64
|
-
const silenceSince = Date.now() - (lastVisibleMs > 0 ? lastVisibleMs : startedMs);
|
|
65
|
-
|
|
66
|
-
if (silenceSince >= FORCE_VISIBLE_UPDATE_MS && engine.messagingManager && !runMeta.terminalInterim) {
|
|
67
|
-
const { platform, chatId } = runMeta.messagingContext || {};
|
|
68
|
-
if (platform && chatId) {
|
|
69
|
-
const stepNum = ledger.currentStep != null ? ledger.currentStep : '?';
|
|
70
|
-
const tool = ledger.currentTool || 'tools';
|
|
71
|
-
const statusMsg = options.stalled
|
|
72
|
-
? 'still here, just taking a while on this one...'
|
|
73
|
-
: `still working on it... (step ${stepNum}, running ${tool})`;
|
|
74
|
-
try {
|
|
75
|
-
await engine.messagingManager.sendMessage(runMeta.userId, platform, chatId, statusMsg, {
|
|
76
|
-
runId,
|
|
77
|
-
agentId: runMeta.agentId,
|
|
78
|
-
});
|
|
79
|
-
const nowIso = isoNow();
|
|
80
|
-
runMeta.progressLedger = { ...ledger, lastUserVisibleUpdateAt: nowIso };
|
|
81
|
-
engine.updateRunProgress(runId, { lastUserVisibleUpdateAt: nowIso });
|
|
82
|
-
engine.recordRunEvent(runMeta.userId, runId, 'forced_progress_update_sent', {
|
|
83
|
-
stepNum, tool, silenceSince, stalled: options.stalled === true,
|
|
84
|
-
}, { agentId: runMeta.agentId });
|
|
85
|
-
return { sent: true, heartbeat: true, forced: true };
|
|
86
|
-
} catch (err) {
|
|
87
|
-
console.warn('[Engine] Forced progress update send failed:', err?.message || err);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
57
|
engine.recordRunEvent(runMeta.userId, runId, 'progress_heartbeat_sent', {
|
|
93
58
|
stalled: options.stalled === true,
|
|
94
59
|
currentTool: ledger.currentTool || null,
|
|
@@ -117,8 +117,20 @@ function isReadOnlyInterpreterCommand(segment = '') {
|
|
|
117
117
|
return /\b(print|json\.|json_tool|json\.load|json\.loads|sys\.stdin|process\.exit|console\.log)\b|-m\s+json\.tool/i.test(normalized);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function hasStateChangingRedirect(segment = '') {
|
|
121
|
+
const matches = String(segment || '').matchAll(/(?:^|[^&|;])(?:>>?|1>)\s*(?:"([^"]+)"|'([^']+)'|(\S+))/g);
|
|
122
|
+
for (const match of matches) {
|
|
123
|
+
const target = String(match[1] || match[2] || match[3] || '').trim();
|
|
124
|
+
if (!target || target === '/dev/null') continue;
|
|
125
|
+
if (target.startsWith('/tmp/') || target.startsWith('/var/tmp/')) continue;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
function isStateChangingShellSegment(segment = '') {
|
|
121
132
|
const normalized = stripEnvAssignments(segment);
|
|
133
|
+
if (hasStateChangingRedirect(normalized)) return true;
|
|
122
134
|
const command = normalizeCommandName(firstToken(normalized));
|
|
123
135
|
if (!command) return false;
|
|
124
136
|
if (command === 'git') {
|
|
@@ -146,6 +158,7 @@ function isProgressToolCall(toolName, toolArgs = {}) {
|
|
|
146
158
|
const name = String(toolName || '');
|
|
147
159
|
if (!name) return false;
|
|
148
160
|
if (name === 'activate_tools' || name === 'save_widget_snapshot') return false;
|
|
161
|
+
if (name === 'send_interim_update') return false;
|
|
149
162
|
if (/^(list_|search_|read_file|get_file|find_files?|github_list|github_get|github_search|browser_get|browser_read)/.test(name)) {
|
|
150
163
|
return false;
|
|
151
164
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const FIRST_UPDATE_MS =
|
|
4
|
-
const REPEAT_UPDATE_MS =
|
|
5
|
-
const STALL_MS =
|
|
3
|
+
const FIRST_UPDATE_MS = 60 * 1000;
|
|
4
|
+
const REPEAT_UPDATE_MS = 90 * 1000;
|
|
5
|
+
const STALL_MS = 240 * 1000;
|
|
6
6
|
const TICK_MS = 15 * 1000;
|
|
7
7
|
|
|
8
8
|
function isoNow() {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const {
|
|
5
|
+
isClearlyReadOnlyShellCommand,
|
|
6
|
+
} = require('./loop/progress_classification');
|
|
4
7
|
|
|
5
8
|
function canonicalize(value) {
|
|
6
9
|
if (Array.isArray(value)) return value.map(canonicalize);
|
|
@@ -20,6 +23,47 @@ function stableHash(value) {
|
|
|
20
23
|
return crypto.createHash('sha256').update(text).digest('hex');
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
function normalizeReadOnlyShellIntent(command = '') {
|
|
27
|
+
const text = String(command || '')
|
|
28
|
+
.replace(/(^|\n)\s*#.*(?=\n|$)/g, '\n')
|
|
29
|
+
.replace(/2?>\s*(?:"[^"]+"|'[^']+'|\S+)/g, '')
|
|
30
|
+
.replace(/\s+/g, ' ')
|
|
31
|
+
.trim();
|
|
32
|
+
const urls = [...text.matchAll(/https?:\/\/[^\s"'`|;&)]+/gi)]
|
|
33
|
+
.map((match) => match[0].replace(/[?#].*$/, ''))
|
|
34
|
+
.sort();
|
|
35
|
+
const paths = [...text.matchAll(/(?:^|\s)(\/[A-Za-z0-9._~/%+-][^\s"'`|;&)]*)/g)]
|
|
36
|
+
.map((match) => match[1].replace(/[?#].*$/, ''))
|
|
37
|
+
.filter((item) => !item.startsWith('/tmp/'))
|
|
38
|
+
.sort();
|
|
39
|
+
const repoSearches = [...text.matchAll(/\brepo:[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/gi)]
|
|
40
|
+
.map((match) => match[0].toLowerCase())
|
|
41
|
+
.sort();
|
|
42
|
+
|
|
43
|
+
if (urls.length || paths.length || repoSearches.length) {
|
|
44
|
+
return {
|
|
45
|
+
kind: 'read_only_shell_intent',
|
|
46
|
+
urls,
|
|
47
|
+
paths,
|
|
48
|
+
repoSearches,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
kind: 'read_only_shell_command',
|
|
54
|
+
command: text
|
|
55
|
+
.replace(/\|\s*(cat|head(?:\s+-n?\s+\d+)?|tail(?:\s+-n?\s+\d+)?|wc(?:\s+-[A-Za-z]+)?)\b[^|;&]*/g, '')
|
|
56
|
+
.trim(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function canonicalToolArgs(toolName, args) {
|
|
61
|
+
if (toolName === 'execute_command' && isClearlyReadOnlyShellCommand(args?.command || '')) {
|
|
62
|
+
return normalizeReadOnlyShellIntent(args.command);
|
|
63
|
+
}
|
|
64
|
+
return args || {};
|
|
65
|
+
}
|
|
66
|
+
|
|
23
67
|
class ToolRepetitionGuard {
|
|
24
68
|
constructor({ unchangedLimit = 2 } = {}) {
|
|
25
69
|
this.unchangedLimit = Math.max(1, Number(unchangedLimit) || 2);
|
|
@@ -27,7 +71,7 @@ class ToolRepetitionGuard {
|
|
|
27
71
|
}
|
|
28
72
|
|
|
29
73
|
key(toolName, args) {
|
|
30
|
-
return `${String(toolName || '')}:${stableHash(args
|
|
74
|
+
return `${String(toolName || '')}:${stableHash(canonicalToolArgs(toolName, args))}`;
|
|
31
75
|
}
|
|
32
76
|
|
|
33
77
|
shouldBlock(toolName, args) {
|
|
@@ -44,7 +88,7 @@ class ToolRepetitionGuard {
|
|
|
44
88
|
: 1;
|
|
45
89
|
const next = {
|
|
46
90
|
toolName,
|
|
47
|
-
argsHash: stableHash(args
|
|
91
|
+
argsHash: stableHash(canonicalToolArgs(toolName, args)),
|
|
48
92
|
resultHash,
|
|
49
93
|
unchangedCount,
|
|
50
94
|
};
|
|
@@ -56,5 +100,6 @@ class ToolRepetitionGuard {
|
|
|
56
100
|
module.exports = {
|
|
57
101
|
ToolRepetitionGuard,
|
|
58
102
|
canonicalize,
|
|
103
|
+
normalizeReadOnlyShellIntent,
|
|
59
104
|
stableHash,
|
|
60
105
|
};
|
|
@@ -82,6 +82,7 @@ const VERIFIER_PROMPT_INSTRUCTIONS = [
|
|
|
82
82
|
const EXECUTION_GUIDANCE_ACTION_LINES = [
|
|
83
83
|
'Act end-to-end. Run independent searches or inspections in parallel when possible. Prefer native integration tools and structured APIs over browser automation or shell scraping. Use exact IDs and required parameters; list or search first when you do not have them.',
|
|
84
84
|
'For GitHub issue implementation or PR work, fetch the issue once, then establish or reuse a writable local checkout, create a task branch, inspect/edit/test locally, and push/open the PR. Use direct GitHub file mutation tools only as a fallback when a local checkout is unavailable.',
|
|
85
|
+
'Tool results are already present in the conversation as tool output. Do not assume a tool result was persisted to a readable /tmp path unless that exact path was explicitly returned by the tool result.',
|
|
85
86
|
'Use send_interim_update sparingly when a short real update or question would help.',
|
|
86
87
|
'When you must ask for missing required user input, ask once, then wait for the reply instead of re-asking in the same run.',
|
|
87
88
|
'For outbound messages, calls, emails, shared edits, installs, restarts, or task mutations, verify the action result before claiming it happened. If user confirmation is required and missing, draft or ask instead of sending.',
|
|
@@ -559,6 +560,9 @@ function buildExecutionGuidance({ analysis, plan = null, capabilityHealth }) {
|
|
|
559
560
|
? `Advisory tool suggestions: ${analysis.suggested_tools.join(', ')}`
|
|
560
561
|
: '',
|
|
561
562
|
`Autonomy contract: complexity=${analysis.complexity || 'standard'}; autonomy_level=${analysis.autonomy_level || 'normal'}; progress_update_policy=${analysis.progress_update_policy || 'optional'}; parallel_work=${analysis.parallel_work === true}; completion_confidence_required=${analysis.completion_confidence_required || 'medium'}.`,
|
|
563
|
+
analysis.progress_update_policy === 'required'
|
|
564
|
+
? 'Progress updates are required for this run: when the work becomes slow, changes method, or hits a temporary blocker, use send_interim_update with a concise factual update. Do not use progress updates as completion, and do not continue long read-only work silently after a progress-check nudge.'
|
|
565
|
+
: '',
|
|
562
566
|
capabilityHealth ? `Capability health:\n${capabilityHealth}` : '',
|
|
563
567
|
];
|
|
564
568
|
|