Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2dd79ae
fix(prompts): add explicit verb-first titles to all studio guide prom…
rafavalls Apr 24, 2026
0639eb5
[release]: bump to 2.274.1
github-actions[bot] Apr 24, 2026
0c2f9a7
fix(deco-sites): avoid duplicate profile insert on service account cr…
guitavano Apr 24, 2026
09a732b
[release]: bump to 2.274.2
github-actions[bot] Apr 24, 2026
0a61c37
feat(analytics): integrate PostHog for server-side and client-side tr…
rafavalls Apr 24, 2026
e54a90b
[release]: bump to 2.275.0
github-actions[bot] Apr 24, 2026
7d2291e
fix(simple-model-mode): gate on provider availability and fix selecto…
rafavalls Apr 24, 2026
5b27249
[release]: bump to 2.275.1
github-actions[bot] Apr 24, 2026
922671d
feat(sandbox): unified sandbox daemon
tlgimenes Apr 25, 2026
a5df48e
feat(git-panel): GitHub PR management panel polish
tlgimenes Apr 25, 2026
32611f6
merge: origin/tlgimenes/vm-start-hang
tlgimenes Apr 25, 2026
b643958
feat(sandbox): add computeHandle for slugified URLs
tlgimenes Apr 25, 2026
2824ba1
test(sandbox): cover whitespace-only branch in computeHandle
tlgimenes Apr 25, 2026
236c0c3
feat(sandbox/freestyle): use slugified handle for preview domain
tlgimenes Apr 25, 2026
ff20bef
feat(sandbox/docker): use slugified handle as container name
tlgimenes Apr 25, 2026
06a664b
test+fix(sandbox/docker): cover adopt path and recover from --name co…
tlgimenes Apr 25, 2026
9c049cd
chore: checkpoint in-flight chat/sandbox work before task-creation re…
tlgimenes Apr 25, 2026
d2b7a57
refactor(thread): move branch-name generator next to create tool
tlgimenes Apr 25, 2026
0ed1d03
feat(thread/schema): require virtual_mcp_id, drop branch from create …
tlgimenes Apr 25, 2026
8877f03
test(thread): add buildThreadTestContext helper
tlgimenes Apr 25, 2026
342b303
feat(thread/create): server-derive branch from vMCP github metadata
tlgimenes Apr 25, 2026
63870f8
feat(storage/threads): idempotent create — ON CONFLICT DO NOTHING ret…
tlgimenes Apr 25, 2026
de58794
feat(thread/update): reject branch=null for github-linked threads
tlgimenes Apr 25, 2026
99268c3
refactor(decopilot/memory): require thread to exist; drop create-on-m…
tlgimenes Apr 25, 2026
ba1a539
feat(web/hooks): add useTask, useTasks, useTaskActions
tlgimenes Apr 25, 2026
c7f089a
feat(web/hooks): add useEnsureTask for create-on-404 routing
tlgimenes Apr 25, 2026
18f5a9c
feat(web/routes): create-on-404 task-route boundary
tlgimenes Apr 25, 2026
8a50159
refactor(chat-context): navigate-only createTask; drop vmcp override
tlgimenes Apr 25, 2026
4a7847d
refactor(chat-nav): collapse virtualMcpOverride into virtualmcpid
tlgimenes Apr 25, 2026
a325e80
refactor(chat/task): drop optimistic createTask, buildOptimisticTask,…
tlgimenes Apr 25, 2026
7bc2065
refactor(web): drop seedNewTask and useCreateTaskAndNavigate; navigat…
tlgimenes Apr 25, 2026
6c97df5
refactor(branch-picker): drop client-side new-branch generator
tlgimenes Apr 25, 2026
f88c5d6
chore(branch-name): drop shim — server-only generation
tlgimenes Apr 25, 2026
8080614
fix(agent-shell-layout): hoist useEnsureTask into the layout
tlgimenes Apr 25, 2026
b6b840c
fix(use-ensure-task): invalidate legacy task list cache after create
tlgimenes Apr 26, 2026
7e13d8b
fix(use-ensure-task): per-id ref guard so back-to-back creates work
tlgimenes Apr 26, 2026
01dbcc8
refactor(use-ensure-task): drop refs in favor of useEffect
tlgimenes Apr 26, 2026
1bf42f1
feat(threads): branch carry-over for "+ New Task" buttons
tlgimenes Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ jobs:
- name: Install dependencies
run: bun install

- name: Build daemon bundle
run: bun run --cwd=packages/sandbox build

- name: Start NATS with JetStream
run: |
docker run -d --name nats -p 4222:4222 nats:2.10 -js
Expand Down Expand Up @@ -135,3 +138,51 @@ jobs:

- name: Run knip
run: bun run knip

docker-smoke:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.5"

- name: Install dependencies
run: bun install

- name: Build daemon bundle
run: bun run --cwd=packages/sandbox build

- name: Build sandbox image
run: |
docker build \
-t mesh-sandbox:ci \
-f packages/sandbox/image/Dockerfile \
packages/sandbox

- name: Smoke test
run: |
docker run -d --name sandbox-smoke -p 19999:9000 \
-e DAEMON_TOKEN="$(printf 't%.0s' {1..32})" \
-e DAEMON_BOOT_ID="ci-smoke" \
-e APP_ROOT=/app \
-e PROXY_PORT=9000 \
-e DAEMON_NO_AUTOSTART=1 \
mesh-sandbox:ci
for i in $(seq 1 30); do
if curl -fsS http://localhost:19999/health | grep -q '"bootId":"ci-smoke"'; then
echo "ok"
exit 0
fi
sleep 1
done
echo "smoke test failed — daemon did not return /health with ci-smoke bootId"
docker logs sandbox-smoke
exit 1

- name: Tear down
if: always()
run: docker rm -f sandbox-smoke || true
2 changes: 1 addition & 1 deletion apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
"kysely-pglite": "^0.6.1",
"lucide-react": "^0.468.0",
"marked": "^15.0.6",
"mesh-plugin-user-sandbox": "workspace:*",
"@decocms/sandbox": "workspace:*",
"mesh-plugin-workflows": "workspace:*",
"nanoid": "^5.1.6",
"pg": "^8.16.3",
Expand Down
2 changes: 1 addition & 1 deletion apps/mesh/spec/monitoring-share-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ setup: (ctx) => {
|------|--------|
| `packages/bindings/src/core/plugins.ts` | Add `rootRoute` and `registerPublicRoutes` to context |
| `apps/mesh/src/web/index.tsx` | Pass new context props, collect and mount public routes |
| `packages/mesh-plugin-user-sandbox/client/index.ts` | Migrate connect route registration |
| `packages/@decocms/sandbox/client/index.ts` | Migrate connect route registration |
| `apps/mesh/src/web/routes/connect.tsx` | Remove (or keep as fallback) |

### Phase 2 & 3 (Plugin)
Expand Down
7 changes: 1 addition & 6 deletions apps/mesh/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import oauthProxyRoutes, {
} from "./routes/oauth-proxy";
import openaiCompatRoutes from "./routes/openai-compat";
import proxyRoutes from "./routes/proxy";
import { createSandboxDaemonRoutes } from "./routes/sandbox-daemon";
import { createKVRoutes } from "./routes/kv";
import { createTriggerCallbackRoutes } from "./routes/trigger-callback";
import publicConfigRoutes from "./routes/public-config";
Expand Down Expand Up @@ -1361,10 +1360,6 @@ export async function createApp(options: CreateAppOptions = {}) {
});
app.route("/api", decopilotRoutes);

// Daemon control-plane passthrough only — dev-server traffic bypasses
// mesh and hits pods' public URLs directly.
app.route("/", createSandboxDaemonRoutes());

// Stable file redirect endpoint (resolves mesh-storage: URIs to presigned URLs)
app.route("/api", filesRoutes);

Expand Down Expand Up @@ -1591,7 +1586,7 @@ export async function createApp(options: CreateAppOptions = {}) {
const dockerRunner = asDockerRunner(getSharedRunnerIfInit());
if (dockerRunner) {
const { sweepDockerOrphansOnShutdown } = await import(
"mesh-plugin-user-sandbox/runner"
"@decocms/sandbox/runner"
);
await sweepDockerOrphansOnShutdown(dockerRunner);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/mesh/src/api/routes/decopilot/built-in-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { createReadResourceTool } from "./resources";
import { createSandboxTool, type VirtualClient } from "./sandbox";
import { createVmTools } from "./vm-tools";
import { getRunnerByKind } from "@/sandbox/lifecycle";
import type { RunnerKind } from "mesh-plugin-user-sandbox/runner";
import type { RunnerKind } from "@decocms/sandbox/runner";
import { createSubtaskTool } from "./subtask";
import { userAskTool } from "./user-ask";
import { proposePlanTool } from "./propose-plan";
Expand Down
25 changes: 14 additions & 11 deletions apps/mesh/src/api/routes/decopilot/built-in-tools/vm-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* VM File Tools — runner-agnostic.
*
* Registers the six LLM-visible tools (read/write/edit/grep/glob/bash) on
* top of any `SandboxRunner.proxyDaemonRequest`. Path scheme is Docker's
* canonical `/_daemon/fs/<op>` + `/_daemon/bash`; non-Docker runners
* translate inside `proxyDaemonRequest` (see Freestyle's `translateDaemonPath`).
* top of any `SandboxRunner.proxyDaemonRequest`. All runners speak the
* unified `/_decopilot_vm/*` surface with base64-wrapped JSON bodies
* (Cloudflare WAF bypass; harmless 33% overhead on non-CF paths).
*/

import { tool, zodSchema } from "ai";
import type { SandboxRunner } from "mesh-plugin-user-sandbox/runner";
import type { SandboxRunner } from "@decocms/sandbox/runner";
import { maybeTruncate } from "./common";
import {
BASH_DESCRIPTION,
Expand Down Expand Up @@ -37,10 +37,13 @@ async function daemonRequest(
): Promise<unknown> {
let res: Response;
try {
const b64Body = Buffer.from(JSON.stringify(body), "utf-8").toString(
"base64",
);
res = await runner.proxyDaemonRequest(handle, path, {
method: "POST",
headers: new Headers({ "content-type": "application/json" }),
body: JSON.stringify(body),
body: b64Body,
});
} catch {
throw new Error(
Expand Down Expand Up @@ -98,7 +101,7 @@ export function createVmTools(params: VmToolsParams) {
description: READ_DESCRIPTION,
inputSchema: zodSchema(ReadInputSchema),
execute: async (input) => {
const result = await call("/_daemon/fs/read", input);
const result = await call("/_decopilot_vm/read", input);
return maybeTruncate(result, toolOutputMap);
},
});
Expand All @@ -107,22 +110,22 @@ export function createVmTools(params: VmToolsParams) {
needsApproval: approvalFor(TOOL_APPROVAL.write),
description: WRITE_DESCRIPTION,
inputSchema: zodSchema(WriteInputSchema),
execute: async (input) => call("/_daemon/fs/write", input),
execute: async (input) => call("/_decopilot_vm/write", input),
});

const edit = tool({
needsApproval: approvalFor(TOOL_APPROVAL.edit),
description: EDIT_DESCRIPTION,
inputSchema: zodSchema(EditInputSchema),
execute: async (input) => call("/_daemon/fs/edit", input),
execute: async (input) => call("/_decopilot_vm/edit", input),
});

const grep = tool({
needsApproval: approvalFor(TOOL_APPROVAL.grep),
description: GREP_DESCRIPTION,
inputSchema: zodSchema(GrepInputSchema),
execute: async (input) => {
const result = await call("/_daemon/fs/grep", input);
const result = await call("/_decopilot_vm/grep", input);
return maybeTruncate(result, toolOutputMap);
},
});
Expand All @@ -132,7 +135,7 @@ export function createVmTools(params: VmToolsParams) {
description: GLOB_DESCRIPTION,
inputSchema: zodSchema(GlobInputSchema),
execute: async (input) => {
const result = await call("/_daemon/fs/glob", input);
const result = await call("/_decopilot_vm/glob", input);
return maybeTruncate(result, toolOutputMap);
},
});
Expand All @@ -142,7 +145,7 @@ export function createVmTools(params: VmToolsParams) {
description: BASH_DESCRIPTION,
inputSchema: zodSchema(BashInputSchema),
execute: async (input) => {
const result = await call("/_daemon/bash", input);
const result = await call("/_decopilot_vm/bash", input);
return maybeTruncate(result, toolOutputMap);
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SandboxRunner } from "mesh-plugin-user-sandbox/runner";
import type { SandboxRunner } from "@decocms/sandbox/runner";

export interface VmToolsParams {
readonly runner: SandboxRunner;
Expand Down
45 changes: 45 additions & 0 deletions apps/mesh/src/api/routes/decopilot/memory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import { createMemory } from "./memory";
import {
buildThreadTestContext,
type ThreadTestEnv,
} from "../../../tools/thread/test-helpers";

describe("createMemory", () => {
let env: ThreadTestEnv;

beforeAll(async () => {
env = await buildThreadTestContext();
});
afterAll(async () => {
await env.close();
});

it("returns Memory when thread exists", async () => {
const thread = await env.ctx.storage.threads.create({
id: "thrd_existing",
organization_id: env.orgId,
title: "ok",
created_by: env.userId,
virtual_mcp_id: "vmcp_x",
});

const memory = await createMemory(env.ctx.storage.threads, {
thread_id: thread.id,
organization_id: env.orgId,
userId: env.userId,
});

expect(memory.thread.id).toBe("thrd_existing");
});

it("throws when thread_id is provided but thread does not exist", async () => {
await expect(
createMemory(env.ctx.storage.threads, {
thread_id: "thrd_does_not_exist",
organization_id: env.orgId,
userId: env.userId,
}),
).rejects.toThrow(/thread.*not.*found/i);
});
});
66 changes: 11 additions & 55 deletions apps/mesh/src/api/routes/decopilot/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@

import type { OrgScopedThreadStorage } from "@/storage/threads";
import type { Thread, ThreadMessage } from "@/storage/types";
import { generatePrefixedId } from "@/shared/utils/generate-id";

/**
* Configuration for creating a Memory instance
*/
export interface MemoryConfig {
/** Thread ID (creates new if not found) */
thread_id?: string | null;
/** Thread ID (required — thread must exist) */
thread_id: string;

/** Organization scope */
organization_id: string;
Expand All @@ -24,19 +23,6 @@ export interface MemoryConfig {

/** Default window size for pruning */
defaultWindowSize?: number;

/** Optional trigger ID for automation-created threads */
triggerId?: string;

/** Virtual MCP ID to associate with the thread */
virtualMcpId?: string;

/**
* Git branch to pin this thread to. Only meaningful for GitHub-linked
* virtualmcps. When set on a brand-new thread, it's persisted on the
* thread row and propagates to VM_START.
*/
branch?: string | null;
}

/**
Expand Down Expand Up @@ -88,53 +74,23 @@ export class Memory {
}

/**
* Create or get a thread, returning a Memory instance
* Get an existing thread by id, returning a Memory instance.
* Throws if the thread does not exist — the route loader is responsible for
* creating threads up-front via COLLECTION_THREADS_CREATE.
*/
export async function createMemory(
storage: OrgScopedThreadStorage,
config: MemoryConfig,
): Promise<Memory> {
const {
thread_id,
organization_id,
userId,
defaultWindowSize,
triggerId,
virtualMcpId,
branch,
} = config;

let thread: Thread;
const { thread_id, defaultWindowSize } = config;

if (!thread_id) {
// Create new thread
thread = await storage.create({
id: generatePrefixedId("thrd"),
organization_id,
created_by: userId,
trigger_id: triggerId ?? null,
virtual_mcp_id: virtualMcpId ?? "",
branch: branch ?? null,
});
} else {
// Try to get existing thread scoped to this org
const existing = await storage.get(thread_id);
throw new Error("createMemory: thread_id is required");
}

if (existing) {
thread = existing;
} else {
// Thread not found — create using the client-provided ID so the
// frontend and server stay in sync (avoids a thread-ID switch in
// onFinish which causes a full re-render cascade).
thread = await storage.create({
id: thread_id,
organization_id,
created_by: userId,
trigger_id: triggerId ?? null,
virtual_mcp_id: virtualMcpId ?? "",
branch: branch ?? null,
});
}
const thread = await storage.get(thread_id);
if (!thread) {
throw new Error(`Thread not found: ${thread_id}`);
}

return new Memory({
Expand Down
10 changes: 5 additions & 5 deletions apps/mesh/src/api/routes/decopilot/run-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ describe("RunRegistry", () => {
created_at: "",
updated_at: "",
created_by: "u1",
updated_by: null,
updated_by: undefined,
hidden: false,
context_start_message_id: null,
run_owner_pod: null,
Expand Down Expand Up @@ -455,7 +455,7 @@ describe("RunRegistry", () => {
created_at: "",
updated_at: "",
created_by: "u1",
updated_by: null,
updated_by: undefined,
hidden: false,
context_start_message_id: null,
run_owner_pod: null,
Expand Down Expand Up @@ -489,7 +489,7 @@ describe("RunRegistry", () => {
created_at: "",
updated_at: "",
created_by: "u1",
updated_by: null,
updated_by: undefined,
hidden: false,
context_start_message_id: null,
run_owner_pod: null,
Expand Down Expand Up @@ -523,7 +523,7 @@ describe("RunRegistry", () => {
created_at: "",
updated_at: "",
created_by: "u1",
updated_by: null,
updated_by: undefined,
hidden: false,
context_start_message_id: null,
run_owner_pod: null,
Expand Down Expand Up @@ -637,7 +637,7 @@ describe("RunRegistry", () => {
created_at: "",
updated_at: "",
created_by: "u1",
updated_by: null,
updated_by: undefined,
hidden: false,
context_start_message_id: null,
run_owner_pod: "dead-pod",
Expand Down
Loading
Loading