tide-commander 1.148.0 → 1.149.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/CHANGELOG.md +17 -0
- package/README.md +6 -3
- package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-Cs4kWugD.js} +1 -1
- package/dist/assets/BossSpawnModal-BudMgJqj.js +1 -0
- package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-Bh6_3qtH.js} +1 -1
- package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-Nf4wQFfz.js} +1 -1
- package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-BfRXRX50.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-CXUtjA0r.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D-_rwicD.js} +1 -1
- package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-CdaXsQK0.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-ZsSVxcWr.js} +2 -2
- package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-BAf-klB2.js} +1 -1
- package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-BjRbh5z4.js} +1 -1
- package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-SaywDZVL.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-jru2Ez9a.js} +1 -1
- package/dist/assets/Scene2DCanvas-NJHoe2Qg.js +1 -0
- package/dist/assets/SceneManager-i9wVMa_V.js +104 -0
- package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-BHfKLzWe.js} +3 -3
- package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-BfVevCSo.js} +1 -1
- package/dist/assets/SpawnModal-29x5z53B.js +1 -0
- package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-MbIByjWD.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-DF0vYaM6.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-EGcKS1Pt.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-BLmejfqe.js} +1 -1
- package/dist/assets/{area-logos-CThPeqIM.js → area-logos-C4fgpLgO.js} +1 -1
- package/dist/assets/grok.ico +0 -0
- package/dist/assets/grok.png +0 -0
- package/dist/assets/{index-h3Cuh2Zn.js → index-B-2H7O_Y.js} +2 -2
- package/dist/assets/{index-DelocTaH.js → index-B9xpLAcH.js} +1 -1
- package/dist/assets/{index-BUN7MVf5.js → index-BZ4wZwRu.js} +3 -3
- package/dist/assets/index-BiiLWTXJ.js +1 -0
- package/dist/assets/{index-maZSt2YW.js → index-C6Kf4ewu.js} +5 -5
- package/dist/assets/{index-Cz-ypmob.js → index-C8CGfmXP.js} +1 -1
- package/dist/assets/index-DThK5_yS.js +5 -0
- package/dist/assets/index-DYgVP0G3.js +2 -0
- package/dist/assets/{index-DOzFBkSH.js → index-DhD3m2Qx.js} +1 -1
- package/dist/assets/main-B0UlZnfk.js +260 -0
- package/dist/assets/main-DeiAIdzy.css +1 -0
- package/dist/assets/{web-CfvEypdU.js → web-CXC8P25z.js} +1 -1
- package/dist/assets/{web-CNjTPilF.js → web-LGEDa25c.js} +1 -1
- package/dist/assets/{web-bcYaAwP6.js → web-NM4dUqWg.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/tools.json +4 -2
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +53 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +125 -4
- package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
- package/dist/src/packages/server/claude/session-loader.js +280 -0
- package/dist/src/packages/server/data/index.js +1 -0
- package/dist/src/packages/server/grok/backend.js +231 -0
- package/dist/src/packages/server/grok/index.js +4 -0
- package/dist/src/packages/server/grok/json-event-parser.js +138 -0
- package/dist/src/packages/server/grok/session-watcher.js +460 -0
- package/dist/src/packages/server/routes/agents.js +7 -2
- package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
- package/dist/src/packages/server/runtime/index.js +1 -0
- package/dist/src/packages/server/services/agent-service.js +26 -5
- package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
- package/dist/src/packages/server/services/runtime-events.js +46 -7
- package/dist/src/packages/server/services/runtime-service.js +19 -3
- package/dist/src/packages/server/utils/tool-formatting.js +40 -12
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +63 -7
- package/dist/src/packages/shared/agent-types.js +10 -0
- package/package.json +1 -1
- package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
- package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
- package/dist/assets/SceneManager-QqKygyaw.js +0 -104
- package/dist/assets/SpawnModal-BOeStihN.js +0 -1
- package/dist/assets/index-DP7kd91h.js +0 -1
- package/dist/assets/index-DRyMARFk.js +0 -5
- package/dist/assets/index-DuWmOZdY.js +0 -2
- package/dist/assets/main-C2zgPci-.js +0 -256
- package/dist/assets/main-C3JO2TRI.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{dc as s}from"./main-B0UlZnfk.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{dc as t}from"./main-B0UlZnfk.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{dc as a}from"./main-B0UlZnfk.js";import{ImpactStyle as i,NotificationType as r}from"./index-B-2H7O_Y.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-B0UlZnfk.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-DeiAIdzy.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -43,11 +43,13 @@
|
|
|
43
43
|
"askFollowupQuestion": "AskFollowupQuestion",
|
|
44
44
|
"askUserQuestion": "AskUserQuestion",
|
|
45
45
|
"attemptCompletion": "AttemptCompletion",
|
|
46
|
-
"listFiles": "
|
|
47
|
-
"searchFiles": "
|
|
46
|
+
"listFiles": "List Files",
|
|
47
|
+
"searchFiles": "Search Files",
|
|
48
48
|
"executeCommand": "ExecuteCommand",
|
|
49
49
|
"spawnAgent": "Spawn Agent",
|
|
50
50
|
"sendInput": "Send Input",
|
|
51
|
+
"sendMessage": "Message Agent",
|
|
52
|
+
"taskOutput": "Task Output",
|
|
51
53
|
"wait": "Wait"
|
|
52
54
|
},
|
|
53
55
|
"showOutputsInline": "Show all bash outputs inline",
|
|
@@ -3,6 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
import { StringDecoder } from 'string_decoder';
|
|
4
4
|
import { createLogger } from '../../utils/logger.js';
|
|
5
5
|
import { isTmuxEnabled, checkTmuxAvailability, spawnInTmux, killTmuxSession, interruptTmuxSession, } from './tmux-helper.js';
|
|
6
|
+
import { startGrokSessionWatcher } from '../../grok/session-watcher.js';
|
|
6
7
|
const log = createLogger('Runner');
|
|
7
8
|
export class RunnerProcessLifecycle {
|
|
8
9
|
backend;
|
|
@@ -25,6 +26,35 @@ export class RunnerProcessLifecycle {
|
|
|
25
26
|
this.recoveryStore = deps.recoveryStore;
|
|
26
27
|
this.onDisableAutoRestart = deps.onDisableAutoRestart;
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Grok headless does not stream tool events on stdout. Tail session files
|
|
31
|
+
* and inject tool_start/tool_result into the same pipeline as stdout events.
|
|
32
|
+
*/
|
|
33
|
+
attachGrokSideChannel(activeProcess, agentId, workingDir, sessionId) {
|
|
34
|
+
if (this.backend.name !== 'grok')
|
|
35
|
+
return;
|
|
36
|
+
const watcher = startGrokSessionWatcher({
|
|
37
|
+
agentId,
|
|
38
|
+
workingDir,
|
|
39
|
+
sessionId,
|
|
40
|
+
startedAt: activeProcess.startTime,
|
|
41
|
+
onEvent: (event) => {
|
|
42
|
+
this.stdoutPipeline.emitStandardEvent(agentId, event);
|
|
43
|
+
},
|
|
44
|
+
onSessionId: (sid) => {
|
|
45
|
+
const proc = this.activeProcesses.get(agentId);
|
|
46
|
+
if (proc && !proc.sessionId) {
|
|
47
|
+
proc.sessionId = sid;
|
|
48
|
+
if (proc.lastRequest && !proc.lastRequest.sessionId) {
|
|
49
|
+
proc.lastRequest.sessionId = sid;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.bus.emit({ type: 'runner.session_id', agentId, sessionId: sid });
|
|
53
|
+
this.callbacks.onSessionId(agentId, sid);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
activeProcess.sideChannelStop = () => watcher.stop();
|
|
57
|
+
}
|
|
28
58
|
async run(request) {
|
|
29
59
|
const { agentId, prompt, workingDir, sessionId, model, effort, useChrome, permissionMode = 'bypass', systemPrompt, forceNewSession, forkSession, customAgent, } = request;
|
|
30
60
|
await this.stop(agentId);
|
|
@@ -90,6 +120,7 @@ export class RunnerProcessLifecycle {
|
|
|
90
120
|
tmuxExpectedCommand: path.basename(executable),
|
|
91
121
|
};
|
|
92
122
|
this.activeProcesses.set(agentId, activeProcess);
|
|
123
|
+
this.attachGrokSideChannel(activeProcess, agentId, workingDir, forceNewSession ? undefined : sessionId);
|
|
93
124
|
// Use file-tailing stdout pipeline for tmux mode
|
|
94
125
|
const tailer = this.stdoutPipeline.handleTmuxLog(agentId, tmuxResult.logFile);
|
|
95
126
|
activeProcess.tmuxTailer = tailer;
|
|
@@ -136,10 +167,25 @@ export class RunnerProcessLifecycle {
|
|
|
136
167
|
turnState: 'processing',
|
|
137
168
|
};
|
|
138
169
|
this.activeProcesses.set(agentId, activeProcess);
|
|
170
|
+
this.attachGrokSideChannel(activeProcess, agentId, workingDir, forceNewSession ? undefined : sessionId);
|
|
139
171
|
const stdoutDone = this.stdoutPipeline.handleStdout(agentId, childProcess);
|
|
140
172
|
this.handleStderr(agentId, childProcess);
|
|
141
173
|
childProcess.on('close', async (code, signal) => {
|
|
142
174
|
await stdoutDone;
|
|
175
|
+
// Keep tailing session files briefly so the last tool_result lines flush to disk
|
|
176
|
+
// after the CLI exits (common race on Grok headless).
|
|
177
|
+
const sideStop = activeProcess.sideChannelStop;
|
|
178
|
+
activeProcess.sideChannelStop = undefined;
|
|
179
|
+
if (sideStop) {
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
try {
|
|
182
|
+
sideStop();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// ignore
|
|
186
|
+
}
|
|
187
|
+
}, 2000);
|
|
188
|
+
}
|
|
143
189
|
this.bus.emit({
|
|
144
190
|
type: 'runner.process_closed',
|
|
145
191
|
agentId,
|
|
@@ -221,6 +267,13 @@ export class RunnerProcessLifecycle {
|
|
|
221
267
|
if (activeProcess.tmuxTailer) {
|
|
222
268
|
activeProcess.tmuxTailer.stop();
|
|
223
269
|
}
|
|
270
|
+
try {
|
|
271
|
+
activeProcess.sideChannelStop?.();
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// ignore
|
|
275
|
+
}
|
|
276
|
+
activeProcess.sideChannelStop = undefined;
|
|
224
277
|
this.activeProcesses.delete(agentId);
|
|
225
278
|
this.activityCallbacks.delete(agentId);
|
|
226
279
|
// tmux mode: kill the tmux session and clean up
|
|
@@ -16,11 +16,24 @@ export class RunnerStdoutPipeline {
|
|
|
16
16
|
// loops (respond → notify → respond → notify → ...). Once the notification is sent,
|
|
17
17
|
// suppress all further text/tool output until a new user message arrives.
|
|
18
18
|
notificationSent = new Set();
|
|
19
|
+
// Streaming thinking blocks share a uuid across token deltas. The UI marks a
|
|
20
|
+
// row as thinking via a single leading `[thinking]` prefix — only the first
|
|
21
|
+
// chunk of each stream should get it, otherwise merges become
|
|
22
|
+
// `[thinking] a[thinking] b…`.
|
|
23
|
+
thinkingStreamPrefixed = new Set();
|
|
24
|
+
// High-frequency token streams (Grok headless) produce dozens of tiny deltas
|
|
25
|
+
// per second. Coalesce them into ~one flush every STREAM_COALESCE_MS so the
|
|
26
|
+
// client doesn't remeasure/redraw the virtual list on every token.
|
|
27
|
+
streamCoalesce = new Map();
|
|
19
28
|
constructor(deps) {
|
|
20
29
|
this.backend = deps.backend;
|
|
21
30
|
this.callbacks = deps.callbacks;
|
|
22
31
|
this.bus = deps.bus;
|
|
23
32
|
}
|
|
33
|
+
/** Backends that emit token-sized streaming-json deltas. */
|
|
34
|
+
shouldCoalesceStreaming() {
|
|
35
|
+
return this.backend.name === 'grok';
|
|
36
|
+
}
|
|
24
37
|
handleStdout(agentId, process) {
|
|
25
38
|
return new Promise((resolve) => {
|
|
26
39
|
const decoder = new StringDecoder('utf8');
|
|
@@ -134,25 +147,75 @@ export class RunnerStdoutPipeline {
|
|
|
134
147
|
case 'init':
|
|
135
148
|
this.lastEmittedText.delete(agentId);
|
|
136
149
|
this.notificationSent.delete(agentId);
|
|
150
|
+
this.clearThinkingPrefixState(agentId);
|
|
137
151
|
this.callbacks.onOutput(agentId, `Session started: ${event.sessionId} (${event.model})`);
|
|
138
152
|
break;
|
|
139
153
|
case 'text':
|
|
140
154
|
if (event.text) {
|
|
141
155
|
// Suppress consecutive identical text (extra safety for OpenCode agentic loop)
|
|
156
|
+
// Skip for streaming chunks — token-level deltas are intentionally small and
|
|
157
|
+
// may legitimately repeat, and they are merged client-side by uuid.
|
|
142
158
|
const prevText = this.lastEmittedText.get(agentId);
|
|
143
|
-
if (prevText && prevText === event.text.trim()) {
|
|
159
|
+
if (!event.isStreaming && prevText && prevText === event.text.trim()) {
|
|
144
160
|
log.log(`[text] Suppressing duplicate text for agent ${agentId.slice(0, 4)}`);
|
|
145
161
|
this.textEmittedInTurn.add(agentId);
|
|
146
162
|
break;
|
|
147
163
|
}
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
if (event.isStreaming && event.uuid && this.shouldCoalesceStreaming()) {
|
|
165
|
+
this.enqueueStreamChunk(agentId, event.uuid, 'text', event.text);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Flush any pending coalesced deltas before a final/non-stream emit
|
|
169
|
+
// so the client never applies final full-text over a stale partial.
|
|
170
|
+
if (event.uuid) {
|
|
171
|
+
this.flushStreamCoalesce(`${agentId}:text:${event.uuid}`);
|
|
172
|
+
}
|
|
173
|
+
if (!event.isStreaming) {
|
|
174
|
+
this.lastEmittedText.set(agentId, event.text.trim());
|
|
175
|
+
}
|
|
176
|
+
this.callbacks.onOutput(agentId, event.text, event.isStreaming, undefined, event.uuid);
|
|
177
|
+
}
|
|
150
178
|
this.textEmittedInTurn.add(agentId);
|
|
151
179
|
}
|
|
152
180
|
break;
|
|
153
181
|
case 'thinking':
|
|
154
182
|
if (event.text) {
|
|
155
|
-
|
|
183
|
+
const streamKey = event.uuid ? `${agentId}:${event.uuid}` : undefined;
|
|
184
|
+
if (!event.isStreaming) {
|
|
185
|
+
if (event.uuid) {
|
|
186
|
+
this.flushStreamCoalesce(`${agentId}:thinking:${event.uuid}`);
|
|
187
|
+
}
|
|
188
|
+
const thinkingOut = event.text.startsWith('[thinking]')
|
|
189
|
+
? event.text
|
|
190
|
+
: `[thinking] ${event.text}`;
|
|
191
|
+
this.callbacks.onOutput(agentId, thinkingOut, false, undefined, event.uuid);
|
|
192
|
+
if (streamKey) {
|
|
193
|
+
this.thinkingStreamPrefixed.delete(streamKey);
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
// Streaming path
|
|
198
|
+
if (event.uuid && this.shouldCoalesceStreaming()) {
|
|
199
|
+
let chunk = event.text;
|
|
200
|
+
if (streamKey && !this.thinkingStreamPrefixed.has(streamKey)) {
|
|
201
|
+
this.thinkingStreamPrefixed.add(streamKey);
|
|
202
|
+
chunk = `[thinking] ${event.text}`;
|
|
203
|
+
}
|
|
204
|
+
this.enqueueStreamChunk(agentId, event.uuid, 'thinking', chunk);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
let thinkingOut = event.text;
|
|
208
|
+
if (streamKey) {
|
|
209
|
+
if (!this.thinkingStreamPrefixed.has(streamKey)) {
|
|
210
|
+
this.thinkingStreamPrefixed.add(streamKey);
|
|
211
|
+
thinkingOut = `[thinking] ${event.text}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (!event.text.startsWith('[thinking]')) {
|
|
215
|
+
thinkingOut = `[thinking] ${event.text}`;
|
|
216
|
+
}
|
|
217
|
+
this.callbacks.onOutput(agentId, thinkingOut, true, undefined, event.uuid);
|
|
218
|
+
}
|
|
156
219
|
}
|
|
157
220
|
break;
|
|
158
221
|
case 'tool_start': {
|
|
@@ -195,6 +258,8 @@ export class RunnerStdoutPipeline {
|
|
|
195
258
|
break;
|
|
196
259
|
}
|
|
197
260
|
case 'step_complete': {
|
|
261
|
+
// Flush any pending coalesced stream chunks before finalizing the turn
|
|
262
|
+
this.flushAllStreamCoalesceForAgent(agentId);
|
|
198
263
|
const hasErrorResultText = this.isLikelyErrorResultText(event.resultText);
|
|
199
264
|
if (event.resultText && (!this.textEmittedInTurn.has(agentId) || hasErrorResultText)) {
|
|
200
265
|
log.log(`[step_complete] Emitting resultText as fallback (no prior text events) for agent ${agentId.slice(0, 4)}`);
|
|
@@ -203,6 +268,8 @@ export class RunnerStdoutPipeline {
|
|
|
203
268
|
else if (event.resultText) {
|
|
204
269
|
log.log(`[step_complete] Skipping resultText (already emitted via text events) for agent ${agentId.slice(0, 4)}`);
|
|
205
270
|
}
|
|
271
|
+
this.textEmittedInTurn.delete(agentId);
|
|
272
|
+
this.clearThinkingPrefixState(agentId);
|
|
206
273
|
if (event.permissionDenials && event.permissionDenials.length > 0) {
|
|
207
274
|
for (const denial of event.permissionDenials) {
|
|
208
275
|
// Suppress the "[System] Permission denied" line for the two tools
|
|
@@ -245,6 +312,60 @@ export class RunnerStdoutPipeline {
|
|
|
245
312
|
break;
|
|
246
313
|
}
|
|
247
314
|
}
|
|
315
|
+
clearThinkingPrefixState(agentId) {
|
|
316
|
+
const prefix = `${agentId}:`;
|
|
317
|
+
for (const key of this.thinkingStreamPrefixed) {
|
|
318
|
+
if (key.startsWith(prefix)) {
|
|
319
|
+
this.thinkingStreamPrefixed.delete(key);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Drop any pending coalesced chunks for this agent
|
|
323
|
+
for (const [key, buf] of this.streamCoalesce) {
|
|
324
|
+
if (buf.agentId === agentId) {
|
|
325
|
+
if (buf.timer)
|
|
326
|
+
clearTimeout(buf.timer);
|
|
327
|
+
this.streamCoalesce.delete(key);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
static STREAM_COALESCE_MS = 80;
|
|
332
|
+
enqueueStreamChunk(agentId, uuid, kind, chunk) {
|
|
333
|
+
const key = `${agentId}:${kind}:${uuid}`;
|
|
334
|
+
let buf = this.streamCoalesce.get(key);
|
|
335
|
+
if (!buf) {
|
|
336
|
+
buf = { agentId, uuid, kind, chunks: [], timer: null };
|
|
337
|
+
this.streamCoalesce.set(key, buf);
|
|
338
|
+
}
|
|
339
|
+
buf.chunks.push(chunk);
|
|
340
|
+
if (buf.timer)
|
|
341
|
+
return;
|
|
342
|
+
buf.timer = setTimeout(() => {
|
|
343
|
+
this.flushStreamCoalesce(key);
|
|
344
|
+
}, RunnerStdoutPipeline.STREAM_COALESCE_MS);
|
|
345
|
+
}
|
|
346
|
+
flushStreamCoalesce(key) {
|
|
347
|
+
const buf = this.streamCoalesce.get(key);
|
|
348
|
+
if (!buf)
|
|
349
|
+
return;
|
|
350
|
+
if (buf.timer) {
|
|
351
|
+
clearTimeout(buf.timer);
|
|
352
|
+
buf.timer = null;
|
|
353
|
+
}
|
|
354
|
+
if (buf.chunks.length === 0) {
|
|
355
|
+
this.streamCoalesce.delete(key);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const text = buf.chunks.join('');
|
|
359
|
+
buf.chunks = [];
|
|
360
|
+
this.streamCoalesce.delete(key);
|
|
361
|
+
this.callbacks.onOutput(buf.agentId, text, true, undefined, buf.uuid);
|
|
362
|
+
}
|
|
363
|
+
flushAllStreamCoalesceForAgent(agentId) {
|
|
364
|
+
const keys = [...this.streamCoalesce.keys()].filter((k) => k.startsWith(`${agentId}:`));
|
|
365
|
+
for (const key of keys) {
|
|
366
|
+
this.flushStreamCoalesce(key);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
248
369
|
isNotificationCurl(toolInput) {
|
|
249
370
|
const cmd = typeof toolInput?.command === 'string' ? toolInput.command : '';
|
|
250
371
|
return cmd.includes('/api/notify');
|
|
@@ -71,6 +71,12 @@ export class RunnerWatchdog {
|
|
|
71
71
|
log.error(`🐕 [WATCHDOG] Agent ${agentId}: tailer drain failed: ${String(err)}`);
|
|
72
72
|
}
|
|
73
73
|
activeProcess.tmuxTailer?.stop();
|
|
74
|
+
try {
|
|
75
|
+
activeProcess.sideChannelStop?.();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
74
80
|
this.recordDeath({
|
|
75
81
|
agentId,
|
|
76
82
|
pid: activeProcess.process.pid ?? 0,
|
|
@@ -125,6 +131,12 @@ export class RunnerWatchdog {
|
|
|
125
131
|
timestamp: Date.now(),
|
|
126
132
|
stderr: this.lastStderr.get(agentId),
|
|
127
133
|
});
|
|
134
|
+
try {
|
|
135
|
+
activeProcess.sideChannelStop?.();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
128
140
|
this.activeProcesses.delete(agentId);
|
|
129
141
|
this.lastStderr.delete(agentId);
|
|
130
142
|
const remaining = this.activeProcesses.size;
|