diff options
Diffstat (limited to 'modules/core/module-aiassist.js')
| -rw-r--r-- | modules/core/module-aiassist.js | 41 |
1 files changed, 31 insertions, 10 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 417e1b6..db4e691 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -824,18 +824,24 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re if (!userMessage) { this.terminal.print('Usage: ai <your-question>', 'warning'); return; } - this._cancelled = false; - this.terminal.setAiBusy(true); + // A delegated run owns its own session (see _isAgentRun below) and must not queue + // behind the main turn: orchestrate-parallel's fan-out used to run strictly one + // sub-task at a time, and a waiter that hit the timeout cleared a lock it did not + // own — which released the main turn's lock while it was still running. + const _agentTag = (opts && opts.agentTag) || null; + if (!_agentTag) this._cancelled = false; const lockStart = Date.now(); - while (this._chatLock) { + while (!_agentTag && this._chatLock) { if (Date.now() - lockStart > this._chatLockTimeoutMs) { - this._chatLock = false; - this.terminal.print('⚠️ Chat lock timed out — force released', 'warning'); - break; + throw new Error("chat lock busy after " + Math.round(this._chatLockTimeoutMs / 1000) + + "s — refusing to release another turn's lock"); } await new Promise(r => setTimeout(r, 50)); } - this._chatLock = true; + if (!_agentTag) this._chatLock = true; + const _prevTag = this._currentTag; + this._currentTag = _agentTag || 'main'; + this.terminal.setAiBusy(true); const taskStart = Date.now(); // predictor durations exclude chat-lock wait this.terminal.markDirty(); // Auto-route context based on intent @@ -992,7 +998,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re try { var om = this.terminal ? this.terminal.ollamaManager : null; var usageData = om && om._usageLog && om._usageLog.length > 0 ? om._usageLog[om._usageLog.length - 1] : null; - this._postRunReview(loopResult, this._toolCallLog.slice(-(loopResult.toolTurns || 0)), usageData); + this._postRunReview(loopResult, this._recentToolLog(_agentTag || 'main', loopResult.toolTurns || 0), usageData); } catch (e) { /* self-improvement is best-effort */ } } // Auto-create skills from repeated errors @@ -1009,7 +1015,8 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re setTimeout(() => this.terminal.playNotificationSound(), 100); } } finally { - this._chatLock = false; + this._currentTag = _prevTag; + if (!_agentTag) this._chatLock = false; this.terminal.setAiBusy(false); // Persist only the user's conversation — agent sessions are ephemeral. if (!_isAgentRun) this._saveContext(); @@ -1019,7 +1026,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re // `recent` was referenced but never declared: the bare identifier threw // ReferenceError inside this finally block, rejecting chat() on every // tool-using turn (shipped in 32608bc, caught by the caller sweep). - const recent = this._toolCallLog.slice(-(loopResult.toolTurns || 0)); + const recent = this._recentToolLog(_agentTag || 'main', loopResult.toolTurns || 0); const toolNames = recent.map(function(e) { return e.name; }).join(', '); this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s): ' + toolNames + ' · returned ' + Math.round(recent.reduce((a, e) => a + e.returnedBytes, 0) / 1024) + 'KB' @@ -1356,6 +1363,19 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re // went 6 → 112 705 tokens across 8 model calls (maxContextTokens is 12 000). Two // limits keep a single turn inside the window: clip each observation as it arrives, // and elide the oldest ones once the running total passes the budget. + // A run may only see the tool calls it made: with concurrent delegated runs the + // raw ring buffer interleaves, and slicing it by toolTurns handed one agent + // another agent's entries. + _recentToolLog(tag, n) { + const want = tag || 'main'; + const out = []; + const cap = Math.max(0, Number(n) || 0); + for (let i = this._toolCallLog.length - 1; i >= 0 && out.length < cap; i--) { + const e = this._toolCallLog[i]; + if ((e.tag || 'main') === want) out.unshift(e); + } + return out; + } _recordToolCall(entry) { const r = String(entry.result || ''); let truncated = false, contentChars = null; @@ -1375,6 +1395,7 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re clippedBytes: Math.max(0, r.length - Number(entry.returnedBytes || r.length)), truncated: truncated, durationMs: entry.durationMs || 0, ok: !entry.error, errorKind: entry.errorKind || null, + tag: entry.tag || this._currentTag || 'main', }; this._toolCallLog.push(rec); if (this._toolCallLog.length > 50) this._toolCallLog.shift(); |