tide-commander 1.150.0 → 1.150.2
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 +20 -0
- package/dist/assets/{BossLogsModal-DvczRUvP.js → BossLogsModal-DClgunJD.js} +1 -1
- package/dist/assets/{BossSpawnModal-B2autFKk.js → BossSpawnModal-C_FZJD09.js} +1 -1
- package/dist/assets/{ControlsModal-BaREMQxc.js → ControlsModal-rfIMbmzt.js} +1 -1
- package/dist/assets/{DockerLogsModal-DZ0udMCo.js → DockerLogsModal-CWMX-ZSk.js} +1 -1
- package/dist/assets/{EmbeddedEditor-CEjr0ZSZ.js → EmbeddedEditor-BlOp1ctO.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-76BP8J8C.js → GmailOAuthSetup-OblNwH22.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-D45GImPI.js → GoogleOAuthSetup-yObPsrIv.js} +1 -1
- package/dist/assets/{IframeModal-De63RfJZ.js → IframeModal-CnqsbsJs.js} +1 -1
- package/dist/assets/{IntegrationsPanel-DYrw1Sqk.js → IntegrationsPanel-D-rcP2Rf.js} +2 -2
- package/dist/assets/{LogViewerModal-DW-2lpln.js → LogViewerModal-BJyowigx.js} +1 -1
- package/dist/assets/{MonitoringModal-B0-XXyd3.js → MonitoringModal-raphhiXZ.js} +1 -1
- package/dist/assets/{PM2LogsModal-DOfOhkCY.js → PM2LogsModal-D8Ovkw2b.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-DNGmIJmM.js → RestoreArchivedAreaModal-rNEBPF0G.js} +1 -1
- package/dist/assets/{Scene2DCanvas-BW1E76CQ.js → Scene2DCanvas-DEU63SHO.js} +1 -1
- package/dist/assets/{SceneManager-DAvc9-Wp.js → SceneManager-B8M7G0IB.js} +1 -1
- package/dist/assets/{SkillsPanel-Br-gLCLA.js → SkillsPanel-DD4Cq68m.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-DG_bIfDu.js → SlackMultiInstanceSetup-C22rxkX1.js} +1 -1
- package/dist/assets/{SpawnModal-lWM8OuZW.js → SpawnModal-DrgRWQql.js} +1 -1
- package/dist/assets/{StatisticsModal-LvA0Fcvn.js → StatisticsModal-CbE2FHRC.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-8YxNXcR3.js → SubordinateAssignmentModal-NXOWA4-P.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-DftIuKUG.js → TriggerManagerPanel-DLJGXA9B.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-oLwyO_C6.js → WorkflowEditorPanel-B8drq_L1.js} +1 -1
- package/dist/assets/{area-logos-mcpQqUMZ.js → area-logos-W-V1BJXF.js} +1 -1
- package/dist/assets/{index-C6-68CXL.js → index--C9FOQUp.js} +3 -3
- package/dist/assets/{index-CIon0DLf.js → index-35zpG6tD.js} +1 -1
- package/dist/assets/{index-Cwt-E2td.js → index-BBKm8pU7.js} +1 -1
- package/dist/assets/{index-O0t07OB_.js → index-BPE5Mi1H.js} +2 -2
- package/dist/assets/{index-Ay7dKyeR.js → index-CvyJsbe_.js} +1 -1
- package/dist/assets/{index-BMw7AAAK.js → index-D1gkgjaB.js} +1 -1
- package/dist/assets/{index-BQpd34Mc.js → index-DENq1s--.js} +1 -1
- package/dist/assets/{index-DFVroqA5.js → index-DeEjSnXC.js} +1 -1
- package/dist/assets/{index-DR6_WeF_.js → index-uazTq6eV.js} +1 -1
- package/dist/assets/main-BsgKYLZr.css +1 -0
- package/dist/assets/{main-D5sqeuwB.js → main-Ce8hSw4G.js} +136 -133
- package/dist/assets/{web-DPETNqOf.js → web-BKeVejC8.js} +1 -1
- package/dist/assets/{web-vG1Smf_N.js → web-CIOB2Dsi.js} +1 -1
- package/dist/assets/{web-BkrshujV.js → web-CVh7vOIA.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/claude/backend.js +51 -37
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +1 -1
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +1 -1
- package/dist/src/packages/server/codex/backend.js +16 -4
- package/dist/src/packages/server/grok/backend.js +17 -5
- package/dist/src/packages/server/grok/session-watcher.js +15 -0
- package/dist/src/packages/server/opencode/backend.js +14 -3
- package/package.json +1 -1
- package/dist/assets/main-JBkwGXfE.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dd as t}from"./main-
|
|
1
|
+
import{dd as t}from"./main-Ce8hSw4G.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{dd as a}from"./main-
|
|
1
|
+
import{dd as a}from"./main-Ce8hSw4G.js";import{ImpactStyle as i,NotificationType as r}from"./index-BPE5Mi1H.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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dd as s}from"./main-
|
|
1
|
+
import{dd as s}from"./main-Ce8hSw4G.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};
|
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-Ce8hSw4G.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-BsgKYLZr.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -122,6 +122,13 @@ export function buildAppendedProjectInstructions(config) {
|
|
|
122
122
|
}
|
|
123
123
|
return sections.join('\n\n');
|
|
124
124
|
}
|
|
125
|
+
function resetClaudeStreamState(state) {
|
|
126
|
+
state.currentStreamMessageId = null;
|
|
127
|
+
state.streamUuids.clear();
|
|
128
|
+
// suppressTextDeltas is intentionally NOT cleared here — only message_start
|
|
129
|
+
// opens a new turn. That way late content_block_delta lines after the final
|
|
130
|
+
// assistant event cannot open a second bubble with a new stream uuid.
|
|
131
|
+
}
|
|
125
132
|
export class ClaudeBackend {
|
|
126
133
|
name = 'claude';
|
|
127
134
|
// ---------------------------------------------------------------------------
|
|
@@ -132,12 +139,26 @@ export class ClaudeBackend {
|
|
|
132
139
|
// emits tiny tokens with no uuid and we mint one). We pin a stable stream id
|
|
133
140
|
// per (message_id, content_block index, kind) so addOutput merges into one
|
|
134
141
|
// terminal row. The final assistant message reuses the same id to finalize.
|
|
142
|
+
//
|
|
143
|
+
// ONE ClaudeBackend instance serves every Claude agent (single runner per
|
|
144
|
+
// provider), so this state is keyed by agentId — otherwise two concurrent
|
|
145
|
+
// agents reset each other's stream uuids mid-turn (dropped deltas, stuck
|
|
146
|
+
// streaming rows, duplicate final bubbles).
|
|
135
147
|
// ---------------------------------------------------------------------------
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
streamStates = new Map();
|
|
149
|
+
streamState(agentId) {
|
|
150
|
+
const key = agentId || '__default';
|
|
151
|
+
let state = this.streamStates.get(key);
|
|
152
|
+
if (!state) {
|
|
153
|
+
state = {
|
|
154
|
+
currentStreamMessageId: null,
|
|
155
|
+
streamUuids: new Map(),
|
|
156
|
+
suppressTextDeltas: false,
|
|
157
|
+
};
|
|
158
|
+
this.streamStates.set(key, state);
|
|
159
|
+
}
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
141
162
|
/**
|
|
142
163
|
* Build CLI arguments for Claude Code
|
|
143
164
|
*/
|
|
@@ -232,7 +253,7 @@ export class ClaudeBackend {
|
|
|
232
253
|
/**
|
|
233
254
|
* Parse Claude CLI raw event into normalized StandardEvent
|
|
234
255
|
*/
|
|
235
|
-
parseEvent(rawEvent) {
|
|
256
|
+
parseEvent(rawEvent, agentId) {
|
|
236
257
|
const event = rawEvent;
|
|
237
258
|
// Log ALL events to understand what we're receiving
|
|
238
259
|
log.log(`parseEvent: type=${event.type}, subtype=${event.subtype || 'none'}, tool_name=${event.tool_name || 'n/a'}`);
|
|
@@ -251,7 +272,7 @@ export class ClaudeBackend {
|
|
|
251
272
|
result = this.parseSystemEvent(event);
|
|
252
273
|
break;
|
|
253
274
|
case 'assistant':
|
|
254
|
-
result = this.parseAssistantEvent(event);
|
|
275
|
+
result = this.parseAssistantEvent(event, agentId);
|
|
255
276
|
break;
|
|
256
277
|
case 'tool_use':
|
|
257
278
|
result = this.parseToolUseEvent(event);
|
|
@@ -260,7 +281,7 @@ export class ClaudeBackend {
|
|
|
260
281
|
result = this.parseResultEvent(event);
|
|
261
282
|
break;
|
|
262
283
|
case 'stream_event':
|
|
263
|
-
result = this.parseStreamEvent(event);
|
|
284
|
+
result = this.parseStreamEvent(event, agentId);
|
|
264
285
|
break;
|
|
265
286
|
case 'user':
|
|
266
287
|
result = this.parseUserEvent(event);
|
|
@@ -437,10 +458,11 @@ export class ClaudeBackend {
|
|
|
437
458
|
}
|
|
438
459
|
return null;
|
|
439
460
|
}
|
|
440
|
-
parseAssistantEvent(event) {
|
|
461
|
+
parseAssistantEvent(event, agentId) {
|
|
441
462
|
// Check for content blocks in assistant message
|
|
442
463
|
// Claude CLI sends both text and tool_use as content blocks within assistant events
|
|
443
464
|
if (event.message?.content && Array.isArray(event.message.content)) {
|
|
465
|
+
const state = this.streamState(agentId);
|
|
444
466
|
const events = [];
|
|
445
467
|
// Use event UUID if available (unique identifier from Claude)
|
|
446
468
|
const uuid = event.uuid;
|
|
@@ -450,8 +472,8 @@ export class ClaudeBackend {
|
|
|
450
472
|
const messageId = typeof event.message?.id === 'string'
|
|
451
473
|
? event.message.id
|
|
452
474
|
: null;
|
|
453
|
-
if (messageId && !
|
|
454
|
-
|
|
475
|
+
if (messageId && !state.currentStreamMessageId) {
|
|
476
|
+
state.currentStreamMessageId = messageId;
|
|
455
477
|
}
|
|
456
478
|
// Extract text blocks - emit as non-streaming final text.
|
|
457
479
|
//
|
|
@@ -464,7 +486,7 @@ export class ClaudeBackend {
|
|
|
464
486
|
// Never wipe stream state on thinking-only assistant events — that used to
|
|
465
487
|
// force the later full-text assistant onto a new uuid → duplicate bubbles.
|
|
466
488
|
const content = event.message.content;
|
|
467
|
-
const textStreamUuids = [...
|
|
489
|
+
const textStreamUuids = [...state.streamUuids.entries()]
|
|
468
490
|
.filter(([key]) => key.startsWith('text:'))
|
|
469
491
|
.sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
|
|
470
492
|
.map(([, id]) => id);
|
|
@@ -475,7 +497,7 @@ export class ClaudeBackend {
|
|
|
475
497
|
if (block?.type === 'text' && block.text && block.text.trim()) {
|
|
476
498
|
// Prefer ordinal text stream uuid, then index key, then outer uuid.
|
|
477
499
|
const streamUuid = textStreamUuids[textStreamOrdinal] ||
|
|
478
|
-
|
|
500
|
+
state.streamUuids.get(`text:${i}`) ||
|
|
479
501
|
null;
|
|
480
502
|
textStreamOrdinal += 1;
|
|
481
503
|
events.push({
|
|
@@ -490,8 +512,8 @@ export class ClaudeBackend {
|
|
|
490
512
|
// Only clear stream state after we actually finalized a text row. Intermediate
|
|
491
513
|
// thinking-only assistant events must leave stream uuids intact for later deltas.
|
|
492
514
|
if (emittedFinalText) {
|
|
493
|
-
|
|
494
|
-
|
|
515
|
+
state.suppressTextDeltas = true;
|
|
516
|
+
resetClaudeStreamState(state);
|
|
495
517
|
}
|
|
496
518
|
// Extract tool_use blocks
|
|
497
519
|
const toolUseBlocks = event.message.content.filter((b) => b.type === 'tool_use');
|
|
@@ -621,37 +643,30 @@ export class ClaudeBackend {
|
|
|
621
643
|
permissionDenials,
|
|
622
644
|
};
|
|
623
645
|
}
|
|
624
|
-
resetStreamState() {
|
|
625
|
-
this.currentStreamMessageId = null;
|
|
626
|
-
this.streamUuids.clear();
|
|
627
|
-
this.streamTextEmitted = false;
|
|
628
|
-
// suppressTextDeltas is intentionally NOT cleared here — only message_start
|
|
629
|
-
// opens a new turn. That way late content_block_delta lines after the final
|
|
630
|
-
// assistant event cannot open a second bubble with a new stream uuid.
|
|
631
|
-
}
|
|
632
646
|
/** Stable uuid so token-sized Claude deltas merge into one terminal row. */
|
|
633
|
-
ensureStreamUuid(kind, index) {
|
|
647
|
+
ensureStreamUuid(state, kind, index) {
|
|
634
648
|
const key = `${kind}:${index}`;
|
|
635
|
-
let id =
|
|
649
|
+
let id = state.streamUuids.get(key);
|
|
636
650
|
if (!id) {
|
|
637
|
-
const msg =
|
|
651
|
+
const msg = state.currentStreamMessageId || 'anon';
|
|
638
652
|
id = `claude-stream-${msg}-${kind}-${index}`;
|
|
639
|
-
|
|
653
|
+
state.streamUuids.set(key, id);
|
|
640
654
|
}
|
|
641
655
|
return id;
|
|
642
656
|
}
|
|
643
|
-
parseStreamEvent(event) {
|
|
657
|
+
parseStreamEvent(event, agentId) {
|
|
644
658
|
const streamEvent = event.event;
|
|
645
659
|
if (!streamEvent)
|
|
646
660
|
return null;
|
|
661
|
+
const state = this.streamState(agentId);
|
|
647
662
|
// Capture Anthropic message id so every delta in this turn shares a stream key.
|
|
648
663
|
if (streamEvent.type === 'message_start') {
|
|
649
664
|
const mid = streamEvent.message?.id;
|
|
650
665
|
// New message — drop any stale stream ids and reopen the stream gate.
|
|
651
|
-
|
|
652
|
-
|
|
666
|
+
state.suppressTextDeltas = false;
|
|
667
|
+
resetClaudeStreamState(state);
|
|
653
668
|
if (typeof mid === 'string' && mid) {
|
|
654
|
-
|
|
669
|
+
state.currentStreamMessageId = mid;
|
|
655
670
|
}
|
|
656
671
|
return null;
|
|
657
672
|
}
|
|
@@ -661,35 +676,34 @@ export class ClaudeBackend {
|
|
|
661
676
|
if (streamEvent.type === 'content_block_delta') {
|
|
662
677
|
if (streamEvent.delta?.type === 'text_delta' && streamEvent.delta.text) {
|
|
663
678
|
// Drop late deltas that race after the full assistant text already finalized.
|
|
664
|
-
if (
|
|
679
|
+
if (state.suppressTextDeltas) {
|
|
665
680
|
return null;
|
|
666
681
|
}
|
|
667
|
-
this.streamTextEmitted = true;
|
|
668
682
|
return {
|
|
669
683
|
type: 'text',
|
|
670
684
|
text: streamEvent.delta.text,
|
|
671
685
|
isStreaming: true,
|
|
672
686
|
// Stable per-block id (NOT event.uuid — those differ every delta).
|
|
673
|
-
uuid: this.ensureStreamUuid('text', blockIndex),
|
|
687
|
+
uuid: this.ensureStreamUuid(state, 'text', blockIndex),
|
|
674
688
|
};
|
|
675
689
|
}
|
|
676
690
|
else if (streamEvent.delta?.type === 'thinking_delta' &&
|
|
677
691
|
streamEvent.delta.text) {
|
|
678
|
-
if (
|
|
692
|
+
if (state.suppressTextDeltas) {
|
|
679
693
|
return null;
|
|
680
694
|
}
|
|
681
695
|
return {
|
|
682
696
|
type: 'thinking',
|
|
683
697
|
text: streamEvent.delta.text,
|
|
684
698
|
isStreaming: true,
|
|
685
|
-
uuid: this.ensureStreamUuid('thinking', blockIndex),
|
|
699
|
+
uuid: this.ensureStreamUuid(state, 'thinking', blockIndex),
|
|
686
700
|
};
|
|
687
701
|
}
|
|
688
702
|
}
|
|
689
703
|
else if (streamEvent.type === 'content_block_start') {
|
|
690
704
|
const blockType = streamEvent.content_block?.type;
|
|
691
705
|
if (blockType === 'text' || blockType === 'thinking') {
|
|
692
|
-
const streamUuid = this.ensureStreamUuid(blockType, blockIndex);
|
|
706
|
+
const streamUuid = this.ensureStreamUuid(state, blockType, blockIndex);
|
|
693
707
|
return {
|
|
694
708
|
type: 'block_start',
|
|
695
709
|
blockType: blockType,
|
|
@@ -43,7 +43,7 @@ export class RunnerProcessLifecycle {
|
|
|
43
43
|
// turn. Finalize open text/thinking streams before the tool card so
|
|
44
44
|
// intermediate status lines stay separate from the final answer.
|
|
45
45
|
if (event.type === 'tool_start' && typeof this.backend.breakOpenStreams === 'function') {
|
|
46
|
-
const breaks = this.backend.breakOpenStreams();
|
|
46
|
+
const breaks = this.backend.breakOpenStreams(agentId);
|
|
47
47
|
for (const ev of breaks) {
|
|
48
48
|
this.stdoutPipeline.emitStandardEvent(agentId, ev);
|
|
49
49
|
}
|
|
@@ -102,7 +102,7 @@ export class RunnerStdoutPipeline {
|
|
|
102
102
|
});
|
|
103
103
|
this.callbacks.onSessionId(agentId, sessionId);
|
|
104
104
|
}
|
|
105
|
-
const eventOrEvents = this.backend.parseEvent(rawEvent);
|
|
105
|
+
const eventOrEvents = this.backend.parseEvent(rawEvent, agentId);
|
|
106
106
|
if (!eventOrEvents) {
|
|
107
107
|
// Even unmapped events (task_progress, thinking_tokens, ...) prove the CLI is
|
|
108
108
|
// alive and mid-work — keep the watchdog's activity clock fresh so a session
|
|
@@ -74,13 +74,25 @@ export function buildCodexPrompt(config) {
|
|
|
74
74
|
}
|
|
75
75
|
export class CodexBackend {
|
|
76
76
|
name = 'codex';
|
|
77
|
-
|
|
77
|
+
// ONE CodexBackend serves every Codex agent — parser state (streamed text
|
|
78
|
+
// accumulators, working dir for diff enrichment) must be per agent or
|
|
79
|
+
// concurrent agents corrupt each other's turn state.
|
|
80
|
+
parsers = new Map();
|
|
78
81
|
// Prompts are passed via stdin (not argv) so large prompts (skills + system
|
|
79
82
|
// prompt + class instructions) don't blow past tmux's ~16KB argv limit,
|
|
80
83
|
// which silently rejects the spawn with "command too long".
|
|
81
84
|
pendingStdinPrompt;
|
|
85
|
+
parserFor(agentId) {
|
|
86
|
+
const key = agentId || '__default';
|
|
87
|
+
let parser = this.parsers.get(key);
|
|
88
|
+
if (!parser) {
|
|
89
|
+
parser = new CodexJsonEventParser({ enableFileDiffEnrichment: true });
|
|
90
|
+
this.parsers.set(key, parser);
|
|
91
|
+
}
|
|
92
|
+
return parser;
|
|
93
|
+
}
|
|
82
94
|
buildArgs(config) {
|
|
83
|
-
this.
|
|
95
|
+
this.parserFor(config.agentId).setWorkingDirectory(config.workingDir);
|
|
84
96
|
this.pendingStdinPrompt = buildCodexPrompt(config);
|
|
85
97
|
const args = ['exec', '--experimental-json'];
|
|
86
98
|
// Codex renamed [features].collab → [features].multi_agent. Enable the new
|
|
@@ -131,8 +143,8 @@ export class CodexBackend {
|
|
|
131
143
|
args.push('-');
|
|
132
144
|
return args;
|
|
133
145
|
}
|
|
134
|
-
parseEvent(rawEvent) {
|
|
135
|
-
const events = this.
|
|
146
|
+
parseEvent(rawEvent, agentId) {
|
|
147
|
+
const events = this.parserFor(agentId).parseEvent(rawEvent);
|
|
136
148
|
if (events.length === 0)
|
|
137
149
|
return null;
|
|
138
150
|
return events.length === 1 ? events[0] : events;
|
|
@@ -77,7 +77,19 @@ function shouldPassGrokModel(model) {
|
|
|
77
77
|
}
|
|
78
78
|
export class GrokBackend {
|
|
79
79
|
name = 'grok';
|
|
80
|
-
parser
|
|
80
|
+
// ONE GrokBackend serves every Grok agent — parser state (stream uuids,
|
|
81
|
+
// accumulated text for breakOpenStreams) must be per agent or concurrent
|
|
82
|
+
// agents leak each other's text into their finalize events.
|
|
83
|
+
parsers = new Map();
|
|
84
|
+
parserFor(agentId) {
|
|
85
|
+
const key = agentId || '__default';
|
|
86
|
+
let parser = this.parsers.get(key);
|
|
87
|
+
if (!parser) {
|
|
88
|
+
parser = new GrokJsonEventParser();
|
|
89
|
+
this.parsers.set(key, parser);
|
|
90
|
+
}
|
|
91
|
+
return parser;
|
|
92
|
+
}
|
|
81
93
|
/** Temp prompt files created for this process; cleaned after formatStdin (no-op) or next buildArgs. */
|
|
82
94
|
lastPromptFile;
|
|
83
95
|
buildArgs(config) {
|
|
@@ -135,15 +147,15 @@ export class GrokBackend {
|
|
|
135
147
|
}
|
|
136
148
|
return args;
|
|
137
149
|
}
|
|
138
|
-
parseEvent(rawEvent) {
|
|
139
|
-
const events = this.
|
|
150
|
+
parseEvent(rawEvent, agentId) {
|
|
151
|
+
const events = this.parserFor(agentId).parseEvent(rawEvent);
|
|
140
152
|
if (events.length === 0)
|
|
141
153
|
return null;
|
|
142
154
|
return events.length === 1 ? events[0] : events;
|
|
143
155
|
}
|
|
144
156
|
/** See CLIBackend.breakOpenStreams — tool-boundary stream split for Grok. */
|
|
145
|
-
breakOpenStreams() {
|
|
146
|
-
return this.
|
|
157
|
+
breakOpenStreams(agentId) {
|
|
158
|
+
return this.parserFor(agentId).breakOpenStreams();
|
|
147
159
|
}
|
|
148
160
|
extractSessionId(rawEvent) {
|
|
149
161
|
const event = rawEvent;
|
|
@@ -184,6 +184,8 @@ export function startGrokSessionWatcher(opts) {
|
|
|
184
184
|
let tailTimer = null;
|
|
185
185
|
/** Dedupe key for last-emitted signals usage snapshot. */
|
|
186
186
|
let lastSignalsUsageKey = '';
|
|
187
|
+
/** mtime+size of signals.json at last read — skip the 200ms-tick read+parse when unchanged. */
|
|
188
|
+
let lastSignalsStatKey = '';
|
|
187
189
|
const emittedToolStarts = new Set();
|
|
188
190
|
const emittedToolResults = new Set();
|
|
189
191
|
/**
|
|
@@ -231,6 +233,18 @@ export function startGrokSessionWatcher(opts) {
|
|
|
231
233
|
const pollSignalsUsage = () => {
|
|
232
234
|
if (!sessionDirPath)
|
|
233
235
|
return;
|
|
236
|
+
// signals.json is rewritten in place; stat is far cheaper than read+parse
|
|
237
|
+
// on every 200ms tick, and the file only changes a few times per turn.
|
|
238
|
+
try {
|
|
239
|
+
const st = fs.statSync(path.join(sessionDirPath, 'signals.json'));
|
|
240
|
+
const statKey = `${st.mtimeMs}:${st.size}`;
|
|
241
|
+
if (statKey === lastSignalsStatKey)
|
|
242
|
+
return;
|
|
243
|
+
lastSignalsStatKey = statKey;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return; // missing file — nothing to read
|
|
247
|
+
}
|
|
234
248
|
const usage = readGrokSignalsUsage(sessionDirPath);
|
|
235
249
|
if (!usage)
|
|
236
250
|
return;
|
|
@@ -412,6 +426,7 @@ export function startGrokSessionWatcher(opts) {
|
|
|
412
426
|
eventsTailer.initAtEof();
|
|
413
427
|
// Seed context bar from existing signals (resume / mid-session attach).
|
|
414
428
|
lastSignalsUsageKey = '';
|
|
429
|
+
lastSignalsStatKey = '';
|
|
415
430
|
pollSignalsUsage();
|
|
416
431
|
if (discoverTimer) {
|
|
417
432
|
clearInterval(discoverTimer);
|
|
@@ -66,8 +66,19 @@ export function buildOpencodePrompt(config) {
|
|
|
66
66
|
}
|
|
67
67
|
export class OpencodeBackend {
|
|
68
68
|
name = 'opencode';
|
|
69
|
-
parser
|
|
69
|
+
// ONE OpencodeBackend serves every OpenCode agent — parser turn state
|
|
70
|
+
// (streamed text accumulators, open stream uuids) must be per agent.
|
|
71
|
+
parsers = new Map();
|
|
70
72
|
pendingStdinPrompt;
|
|
73
|
+
parserFor(agentId) {
|
|
74
|
+
const key = agentId || '__default';
|
|
75
|
+
let parser = this.parsers.get(key);
|
|
76
|
+
if (!parser) {
|
|
77
|
+
parser = new OpencodeJsonEventParser();
|
|
78
|
+
this.parsers.set(key, parser);
|
|
79
|
+
}
|
|
80
|
+
return parser;
|
|
81
|
+
}
|
|
71
82
|
buildArgs(config) {
|
|
72
83
|
const args = ['run', '--format', 'json'];
|
|
73
84
|
// Always skip permissions for autonomous agents
|
|
@@ -91,8 +102,8 @@ export class OpencodeBackend {
|
|
|
91
102
|
log.log(`buildArgs: ${args.length} args, sessionId=${config.sessionId ? 'yes' : 'no'}, model=${config.model || 'default'}`);
|
|
92
103
|
return args;
|
|
93
104
|
}
|
|
94
|
-
parseEvent(rawEvent) {
|
|
95
|
-
const events = this.
|
|
105
|
+
parseEvent(rawEvent, agentId) {
|
|
106
|
+
const events = this.parserFor(agentId).parseEvent(rawEvent);
|
|
96
107
|
if (events.length === 0)
|
|
97
108
|
return null;
|
|
98
109
|
return events.length === 1 ? events[0] : events;
|