From bbddd58f98a08610a3b880f97d1773cb6f03e466 Mon Sep 17 00:00:00 2001 From: Tim Olson Date: Fri, 17 Apr 2026 20:49:21 -0400 Subject: [PATCH] bugfixes --- gateway/src/auth/auth-service.ts | 7 +++-- gateway/src/harness/agent-harness.ts | 13 +++++++-- gateway/src/tools/mcp/mcp-tool-wrapper.ts | 17 +++++++----- sandbox/dexorder/utils.py | 25 ++++++++++++++--- sandbox/main.py | 33 ++++++++++++++++++++++- web/src/components/ChatPanel.vue | 30 ++++++++++++++++++--- 6 files changed, 106 insertions(+), 19 deletions(-) diff --git a/gateway/src/auth/auth-service.ts b/gateway/src/auth/auth-service.ts index 9ff022c0..d57e3155 100644 --- a/gateway/src/auth/auth-service.ts +++ b/gateway/src/auth/auth-service.ts @@ -177,10 +177,9 @@ export class AuthService { async getSession(token: string) { try { const session = await this.config.auth.api.getSession({ - headers: { - // Better Auth expects the session token in the cookie header - cookie: `better-auth.session_token=${token}`, - }, + headers: new Headers({ + 'Authorization': `Bearer ${token}`, + }), }); return session; diff --git a/gateway/src/harness/agent-harness.ts b/gateway/src/harness/agent-harness.ts index 4178a58d..5df4c454 100644 --- a/gateway/src/harness/agent-harness.ts +++ b/gateway/src/harness/agent-harness.ts @@ -264,7 +264,12 @@ export class AgentHarness { this.mcpClient, this.availableMCPTools, this.workspaceManager, - (img) => this.researchImageCapture.push(img) + (img) => this.researchImageCapture.push(img), + (storeName, newState) => { + this.workspaceManager?.setState(storeName, newState).catch((err) => + this.config.logger.error({ err, storeName }, 'Failed to sync workspace after research mutation') + ); + } ); // Inject web_explore tool if the web-explore subagent is ready @@ -475,7 +480,11 @@ export class AgentHarness { this.availableMCPTools, this.workspaceManager, undefined, - undefined + (storeName, newState) => { + this.workspaceManager?.setState(storeName, newState).catch((err) => + this.config.logger.error({ err, storeName }, 'Failed to sync workspace after strategy mutation') + ); + } ); const strategySubagentPath = join(__dirname, 'subagents', 'strategy'); diff --git a/gateway/src/tools/mcp/mcp-tool-wrapper.ts b/gateway/src/tools/mcp/mcp-tool-wrapper.ts index e26d913a..d0330043 100644 --- a/gateway/src/tools/mcp/mcp-tool-wrapper.ts +++ b/gateway/src/tools/mcp/mcp-tool-wrapper.ts @@ -45,21 +45,26 @@ export function createMCPToolWrapper( // Fire workspace mutation callback when workspace_patch or workspace_write succeeds. // The sandbox returns {"success": true, "data": } as a text content item. - if ( - onWorkspaceMutation && - (toolInfo.name === 'workspace_patch' || toolInfo.name === 'workspace_write') - ) { + if (onWorkspaceMutation) { const content = (result as any)?.content; if (Array.isArray(content)) { for (const item of content) { if (item.type === 'text' && item.text) { try { const parsed = JSON.parse(item.text); - if (parsed?.success && parsed?.data !== undefined) { + // workspace_patch / workspace_write: {"success": true, "data": } + if ( + (toolInfo.name === 'workspace_patch' || toolInfo.name === 'workspace_write') && + parsed?.success && parsed?.data !== undefined + ) { onWorkspaceMutation((input as any).store_name as string, parsed.data); } + // python_write / python_edit / python_delete / python_revert: + // {"_workspace_sync": {"store": , "data": }} + if (parsed?._workspace_sync?.store && parsed._workspace_sync.data !== undefined) { + onWorkspaceMutation(parsed._workspace_sync.store, parsed._workspace_sync.data); + } } catch { /* ignore parse errors */ } - break; // only need first text item } } } diff --git a/sandbox/dexorder/utils.py b/sandbox/dexorder/utils.py index f971a9a5..0b8b471d 100644 --- a/sandbox/dexorder/utils.py +++ b/sandbox/dexorder/utils.py @@ -6,10 +6,12 @@ All internal timestamps use nanoseconds since epoch (UTC). """ import logging +import re from typing import Union -from datetime import datetime +from datetime import datetime, timezone import pandas as pd -from dateutil import parser as dateparser +from dateutil import parser as _dateutil_parser +from dateutil.relativedelta import relativedelta log = logging.getLogger(__name__) @@ -18,6 +20,20 @@ TimestampInput = Union[int, float, str, datetime, pd.Timestamp] NANOS_PER_SECOND = 1_000_000_000 +_RELATIVE_RE = re.compile( + r'^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$', + re.IGNORECASE, +) + + +def _parse_relative_date(s: str) -> datetime | None: + """Parse relative date strings like '30 days ago', '2 weeks ago'.""" + m = _RELATIVE_RE.match(s.strip()) + if not m: + return None + n, unit = int(m.group(1)), m.group(2).lower() + return datetime.now(timezone.utc) - relativedelta(**{f'{unit}s': n}) + def to_nanoseconds(timestamp: TimestampInput) -> int: """ @@ -31,6 +47,7 @@ def to_nanoseconds(timestamp: TimestampInput) -> int: timestamp: Can be: - Unix timestamp (int/float) - assumed to be in seconds - ISO date string (str) - parsed using dateutil + - Relative date string (str) - e.g. "30 days ago", "2 weeks ago" - datetime object - pandas Timestamp @@ -48,7 +65,9 @@ def to_nanoseconds(timestamp: TimestampInput) -> int: if isinstance(timestamp, (int, float)): return int(timestamp * NANOS_PER_SECOND) elif isinstance(timestamp, str): - dt = dateparser.parse(timestamp) + dt = _parse_relative_date(timestamp) + if dt is None: + dt = _dateutil_parser.parse(timestamp) if dt is None: raise ValueError(f"Could not parse date string: {timestamp}") return int(dt.timestamp() * NANOS_PER_SECOND) diff --git a/sandbox/main.py b/sandbox/main.py index de484864..1211986a 100644 --- a/sandbox/main.py +++ b/sandbox/main.py @@ -155,6 +155,21 @@ def _remove_indicator_instances(workspace_store, pandas_ta_name: str) -> None: logging.warning(f"Failed to remove indicator instances for {pandas_ta_name}", exc_info=True) +def _workspace_sync_content(workspace_store, category: str) -> "TextContent | None": + """ + Return a TextContent item carrying the current {category}_types workspace state so the + gateway can sync it to connected web clients without a separate workspace_patch call. + The gateway detects items of the form {"_workspace_sync": {"store": ..., "data": ...}}. + """ + store = _type_store_name(category) + result = workspace_store.read(store) + if not result.get('exists'): + return None + import json as _json + payload = _json.dumps({"_workspace_sync": {"store": store, "data": result.get("data")}}) + return TextContent(type="text", text=payload) + + def _populate_types_from_disk(workspace_store, category_manager, category: str) -> None: """Scan existing category items and add any missing entries to the {category}_types store.""" store = _type_store_name(category) @@ -921,6 +936,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server if result.get("success"): _upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", "")) await cleanup_extra_packages_async(get_data_dir(), _get_env_yml()) + sync = _workspace_sync_content(workspace_store, arguments.get("category", "")) + if sync: + content.append(sync) return content elif name == "python_edit": result = await category_manager.edit( @@ -952,6 +970,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server if result.get("success"): _upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", "")) await cleanup_extra_packages_async(get_data_dir(), _get_env_yml()) + sync = _workspace_sync_content(workspace_store, arguments.get("category", "")) + if sync: + content.append(sync) return content elif name == "python_read": return category_manager.read( @@ -987,6 +1008,11 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server meta_parts.append(f"validation errors: {result['validation'].get('errors', [])}") if result.get("success"): _upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", "")) + sync = _workspace_sync_content(workspace_store, arguments.get("category", "")) + content_out = [TextContent(type="text", text="\n".join(meta_parts))] + if sync: + content_out.append(sync) + return content_out return [TextContent(type="text", text="\n".join(meta_parts))] elif name == "python_delete": result = await category_manager.delete( @@ -1002,7 +1028,12 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server for k in ("category", "name", "revision", "packages_removed", "error"): if result.get(k): parts.append(f"{k}: {result[k]}") - return [TextContent(type="text", text="\n".join(parts))] + content_out = [TextContent(type="text", text="\n".join(parts))] + if result.get("success"): + sync = _workspace_sync_content(workspace_store, arguments.get("category", "")) + if sync: + content_out.append(sync) + return content_out elif name == "conda_sync": return await sync_packages_async( data_dir=get_data_dir(), diff --git a/web/src/components/ChatPanel.vue b/web/src/components/ChatPanel.vue index c8e66348..abd392fb 100644 --- a/web/src/components/ChatPanel.vue +++ b/web/src/components/ChatPanel.vue @@ -653,10 +653,10 @@ onUnmounted(() => { v-if="isAgentProcessing" slot="send-icon" @click.stop="stopAgent" - style="display:flex;align-items:center;justify-content:center;width:100%;height:100%" + class="stop-btn" > - - + + @@ -664,6 +664,30 @@ onUnmounted(() => {