mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
Merge remote-tracking branch 'origin/main' into foundry-terminal-pane
# Conflicts: # factory/packages/backend/src/driver.ts # factory/packages/backend/src/integrations/sandbox-agent/client.ts # factory/packages/backend/test/helpers/test-driver.ts # factory/packages/frontend/src/components/mock-layout.tsx # pnpm-lock.yaml # sdks/react/src/ProcessTerminal.tsx
This commit is contained in:
commit
b00c0109d0
288 changed files with 7048 additions and 9134 deletions
31
.github/workflows/ci.yaml
vendored
31
.github/workflows/ci.yaml
vendored
|
|
@ -11,6 +11,8 @@ jobs:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
@ -21,6 +23,35 @@ jobs:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
|
- name: Run formatter hooks
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
git fetch origin "${{ github.base_ref }}" --depth=1
|
||||||
|
diff_range="origin/${{ github.base_ref }}...HEAD"
|
||||||
|
elif [ "${{ github.event_name }}" = "push" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then
|
||||||
|
diff_range="${{ github.event.before }}...${{ github.sha }}"
|
||||||
|
else
|
||||||
|
diff_range="HEAD^...HEAD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t changed_files < <(
|
||||||
|
git diff --name-only --diff-filter=ACMR "$diff_range" \
|
||||||
|
| grep -E '\.(cjs|cts|js|jsx|json|jsonc|mjs|mts|rs|ts|tsx)$' \
|
||||||
|
|| true
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ ${#changed_files[@]} -eq 0 ]; then
|
||||||
|
echo "No formatter-managed files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
args=()
|
||||||
|
for file in "${changed_files[@]}"; do
|
||||||
|
args+=(--file "$file")
|
||||||
|
done
|
||||||
|
|
||||||
|
pnpm exec lefthook run pre-commit --no-stage-fixed --fail-on-changes "${args[@]}"
|
||||||
- run: npm install -g tsx
|
- run: npm install -g tsx
|
||||||
- name: Run checks
|
- name: Run checks
|
||||||
run: ./scripts/release/main.ts --version 0.0.0 --only-steps run-ci-checks
|
run: ./scripts/release/main.ts --version 0.0.0 --only-steps run-ci-checks
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"everything": {
|
"everything": {
|
||||||
"args": [
|
"args": ["@modelcontextprotocol/server-everything"],
|
||||||
"@modelcontextprotocol/server-everything"
|
|
||||||
],
|
|
||||||
"command": "npx"
|
"command": "npx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
biome.json
Normal file
7
biome.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||||
|
"formatter": {
|
||||||
|
"indentStyle": "space",
|
||||||
|
"lineWidth": 160
|
||||||
|
}
|
||||||
|
}
|
||||||
246
docs/docs.json
246
docs/docs.json
|
|
@ -1,131 +1,119 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://mintlify.com/docs.json",
|
"$schema": "https://mintlify.com/docs.json",
|
||||||
"theme": "willow",
|
"theme": "willow",
|
||||||
"name": "Sandbox Agent SDK",
|
"name": "Sandbox Agent SDK",
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"default": "dark",
|
"default": "dark",
|
||||||
"strict": true
|
"strict": true
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"primary": "#ff4f00",
|
"primary": "#ff4f00",
|
||||||
"light": "#ff4f00",
|
"light": "#ff4f00",
|
||||||
"dark": "#ff4f00"
|
"dark": "#ff4f00"
|
||||||
},
|
},
|
||||||
"favicon": "/favicon.svg",
|
"favicon": "/favicon.svg",
|
||||||
"logo": {
|
"logo": {
|
||||||
"light": "/logo/light.svg",
|
"light": "/logo/light.svg",
|
||||||
"dark": "/logo/dark.svg"
|
"dark": "/logo/dark.svg"
|
||||||
},
|
},
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"posthog": {
|
"posthog": {
|
||||||
"apiKey": "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo",
|
"apiKey": "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo",
|
||||||
"apiHost": "https://ph.rivet.gg",
|
"apiHost": "https://ph.rivet.gg",
|
||||||
"sessionRecording": true
|
"sessionRecording": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"label": "Gigacode",
|
"label": "Gigacode",
|
||||||
"icon": "terminal",
|
"icon": "terminal",
|
||||||
"href": "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode"
|
"href": "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Discord",
|
"label": "Discord",
|
||||||
"icon": "discord",
|
"icon": "discord",
|
||||||
"href": "https://discord.gg/auCecybynK"
|
"href": "https://discord.gg/auCecybynK"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"href": "https://github.com/rivet-dev/sandbox-agent"
|
"href": "https://github.com/rivet-dev/sandbox-agent"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"tabs": [
|
"tabs": [
|
||||||
{
|
{
|
||||||
"tab": "Documentation",
|
"tab": "Documentation",
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"group": "Getting started",
|
"group": "Getting started",
|
||||||
"pages": [
|
"pages": [
|
||||||
"quickstart",
|
"quickstart",
|
||||||
"sdk-overview",
|
"sdk-overview",
|
||||||
"react-components",
|
"react-components",
|
||||||
{
|
{
|
||||||
"group": "Deploy",
|
"group": "Deploy",
|
||||||
"icon": "server",
|
"icon": "server",
|
||||||
"pages": [
|
"pages": [
|
||||||
"deploy/local",
|
"deploy/local",
|
||||||
"deploy/computesdk",
|
"deploy/computesdk",
|
||||||
"deploy/e2b",
|
"deploy/e2b",
|
||||||
"deploy/daytona",
|
"deploy/daytona",
|
||||||
"deploy/vercel",
|
"deploy/vercel",
|
||||||
"deploy/cloudflare",
|
"deploy/cloudflare",
|
||||||
"deploy/docker",
|
"deploy/docker",
|
||||||
"deploy/boxlite"
|
"deploy/boxlite"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Agent",
|
"group": "Agent",
|
||||||
"pages": [
|
"pages": ["agent-sessions", "attachments", "skills-config", "mcp-config", "custom-tools"]
|
||||||
"agent-sessions",
|
},
|
||||||
"attachments",
|
{
|
||||||
"skills-config",
|
"group": "System",
|
||||||
"mcp-config",
|
"pages": ["file-system", "processes"]
|
||||||
"custom-tools"
|
},
|
||||||
]
|
{
|
||||||
},
|
"group": "Orchestration",
|
||||||
{
|
"pages": ["architecture", "session-persistence", "observability", "multiplayer", "security"]
|
||||||
"group": "System",
|
},
|
||||||
"pages": ["file-system", "processes"]
|
{
|
||||||
},
|
"group": "Reference",
|
||||||
{
|
"pages": [
|
||||||
"group": "Orchestration",
|
"agent-capabilities",
|
||||||
"pages": [
|
"cli",
|
||||||
"architecture",
|
"inspector",
|
||||||
"session-persistence",
|
"opencode-compatibility",
|
||||||
"observability",
|
{
|
||||||
"multiplayer",
|
"group": "More",
|
||||||
"security"
|
"pages": [
|
||||||
]
|
"credentials",
|
||||||
},
|
"daemon",
|
||||||
{
|
"cors",
|
||||||
"group": "Reference",
|
"session-restoration",
|
||||||
"pages": [
|
"telemetry",
|
||||||
"agent-capabilities",
|
{
|
||||||
"cli",
|
"group": "AI",
|
||||||
"inspector",
|
"pages": ["ai/skill", "ai/llms-txt"]
|
||||||
"opencode-compatibility",
|
}
|
||||||
{
|
]
|
||||||
"group": "More",
|
}
|
||||||
"pages": [
|
]
|
||||||
"credentials",
|
}
|
||||||
"daemon",
|
]
|
||||||
"cors",
|
},
|
||||||
"session-restoration",
|
{
|
||||||
"telemetry",
|
"tab": "HTTP API",
|
||||||
{
|
"pages": [
|
||||||
"group": "AI",
|
{
|
||||||
"pages": ["ai/skill", "ai/llms-txt"]
|
"group": "HTTP Reference",
|
||||||
}
|
"openapi": "openapi.json"
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"tab": "HTTP API",
|
|
||||||
"pages": [
|
|
||||||
{
|
|
||||||
"group": "HTTP Reference",
|
|
||||||
"openapi": "openapi.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"/v1/acp": {
|
"/v1/acp": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_acp_servers",
|
"operationId": "get_v1_acp_servers",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
|
|
@ -40,9 +38,7 @@
|
||||||
},
|
},
|
||||||
"/v1/acp/{server_id}": {
|
"/v1/acp/{server_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_acp",
|
"operationId": "get_v1_acp",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -92,9 +88,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "post_v1_acp",
|
"operationId": "post_v1_acp",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -204,9 +198,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "delete_v1_acp",
|
"operationId": "delete_v1_acp",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -228,9 +220,7 @@
|
||||||
},
|
},
|
||||||
"/v1/agents": {
|
"/v1/agents": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_agents",
|
"operationId": "get_v1_agents",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -280,9 +270,7 @@
|
||||||
},
|
},
|
||||||
"/v1/agents/{agent}": {
|
"/v1/agents/{agent}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_agent",
|
"operationId": "get_v1_agent",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -351,9 +339,7 @@
|
||||||
},
|
},
|
||||||
"/v1/agents/{agent}/install": {
|
"/v1/agents/{agent}/install": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "post_v1_agent_install",
|
"operationId": "post_v1_agent_install",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -412,9 +398,7 @@
|
||||||
},
|
},
|
||||||
"/v1/config/mcp": {
|
"/v1/config/mcp": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_config_mcp",
|
"operationId": "get_v1_config_mcp",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -460,9 +444,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "put_v1_config_mcp",
|
"operationId": "put_v1_config_mcp",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -501,9 +483,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "delete_v1_config_mcp",
|
"operationId": "delete_v1_config_mcp",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -534,9 +514,7 @@
|
||||||
},
|
},
|
||||||
"/v1/config/skills": {
|
"/v1/config/skills": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_config_skills",
|
"operationId": "get_v1_config_skills",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -582,9 +560,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "put_v1_config_skills",
|
"operationId": "put_v1_config_skills",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -623,9 +599,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "delete_v1_config_skills",
|
"operationId": "delete_v1_config_skills",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -656,9 +630,7 @@
|
||||||
},
|
},
|
||||||
"/v1/fs/entries": {
|
"/v1/fs/entries": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_fs_entries",
|
"operationId": "get_v1_fs_entries",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -691,9 +663,7 @@
|
||||||
},
|
},
|
||||||
"/v1/fs/entry": {
|
"/v1/fs/entry": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "delete_v1_fs_entry",
|
"operationId": "delete_v1_fs_entry",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -732,9 +702,7 @@
|
||||||
},
|
},
|
||||||
"/v1/fs/file": {
|
"/v1/fs/file": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_fs_file",
|
"operationId": "get_v1_fs_file",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -754,9 +722,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "put_v1_fs_file",
|
"operationId": "put_v1_fs_file",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -796,9 +762,7 @@
|
||||||
},
|
},
|
||||||
"/v1/fs/mkdir": {
|
"/v1/fs/mkdir": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "post_v1_fs_mkdir",
|
"operationId": "post_v1_fs_mkdir",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -827,9 +791,7 @@
|
||||||
},
|
},
|
||||||
"/v1/fs/move": {
|
"/v1/fs/move": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "post_v1_fs_move",
|
"operationId": "post_v1_fs_move",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
|
|
@ -857,9 +819,7 @@
|
||||||
},
|
},
|
||||||
"/v1/fs/stat": {
|
"/v1/fs/stat": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_fs_stat",
|
"operationId": "get_v1_fs_stat",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -888,9 +848,7 @@
|
||||||
},
|
},
|
||||||
"/v1/fs/upload-batch": {
|
"/v1/fs/upload-batch": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "post_v1_fs_upload_batch",
|
"operationId": "post_v1_fs_upload_batch",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|
@ -931,9 +889,7 @@
|
||||||
},
|
},
|
||||||
"/v1/health": {
|
"/v1/health": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"operationId": "get_v1_health",
|
"operationId": "get_v1_health",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
|
|
@ -951,9 +907,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes": {
|
"/v1/processes": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "List all managed processes.",
|
"summary": "List all managed processes.",
|
||||||
"description": "Returns a list of all processes (running and exited) currently tracked\nby the runtime, sorted by process ID.",
|
"description": "Returns a list of all processes (running and exited) currently tracked\nby the runtime, sorted by process ID.",
|
||||||
"operationId": "get_v1_processes",
|
"operationId": "get_v1_processes",
|
||||||
|
|
@ -981,9 +935,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Create a long-lived managed process.",
|
"summary": "Create a long-lived managed process.",
|
||||||
"description": "Spawns a new process with the given command and arguments. Supports both\npipe-based and PTY (tty) modes. Returns the process descriptor on success.",
|
"description": "Spawns a new process with the given command and arguments. Supports both\npipe-based and PTY (tty) modes. Returns the process descriptor on success.",
|
||||||
"operationId": "post_v1_processes",
|
"operationId": "post_v1_processes",
|
||||||
|
|
@ -1043,9 +995,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/config": {
|
"/v1/processes/config": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Get process runtime configuration.",
|
"summary": "Get process runtime configuration.",
|
||||||
"description": "Returns the current runtime configuration for the process management API,\nincluding limits for concurrency, timeouts, and buffer sizes.",
|
"description": "Returns the current runtime configuration for the process management API,\nincluding limits for concurrency, timeouts, and buffer sizes.",
|
||||||
"operationId": "get_v1_processes_config",
|
"operationId": "get_v1_processes_config",
|
||||||
|
|
@ -1073,9 +1023,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Update process runtime configuration.",
|
"summary": "Update process runtime configuration.",
|
||||||
"description": "Replaces the runtime configuration for the process management API.\nValidates that all values are non-zero and clamps default timeout to max.",
|
"description": "Replaces the runtime configuration for the process management API.\nValidates that all values are non-zero and clamps default timeout to max.",
|
||||||
"operationId": "post_v1_processes_config",
|
"operationId": "post_v1_processes_config",
|
||||||
|
|
@ -1125,9 +1073,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/run": {
|
"/v1/processes/run": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Run a one-shot command.",
|
"summary": "Run a one-shot command.",
|
||||||
"description": "Executes a command to completion and returns its stdout, stderr, exit code,\nand duration. Supports configurable timeout and output size limits.",
|
"description": "Executes a command to completion and returns its stdout, stderr, exit code,\nand duration. Supports configurable timeout and output size limits.",
|
||||||
"operationId": "post_v1_processes_run",
|
"operationId": "post_v1_processes_run",
|
||||||
|
|
@ -1177,9 +1123,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/{id}": {
|
"/v1/processes/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Get a single process by ID.",
|
"summary": "Get a single process by ID.",
|
||||||
"description": "Returns the current state of a managed process including its status,\nPID, exit code, and creation/exit timestamps.",
|
"description": "Returns the current state of a managed process including its status,\nPID, exit code, and creation/exit timestamps.",
|
||||||
"operationId": "get_v1_process",
|
"operationId": "get_v1_process",
|
||||||
|
|
@ -1228,9 +1172,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Delete a process record.",
|
"summary": "Delete a process record.",
|
||||||
"description": "Removes a stopped process from the runtime. Returns 409 if the process\nis still running; stop or kill it first.",
|
"description": "Removes a stopped process from the runtime. Returns 409 if the process\nis still running; stop or kill it first.",
|
||||||
"operationId": "delete_v1_process",
|
"operationId": "delete_v1_process",
|
||||||
|
|
@ -1284,9 +1226,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/{id}/input": {
|
"/v1/processes/{id}/input": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Write input to a process.",
|
"summary": "Write input to a process.",
|
||||||
"description": "Sends data to a process's stdin (pipe mode) or PTY writer (tty mode).\nData can be encoded as base64, utf8, or text. Returns 413 if the decoded\npayload exceeds the configured `maxInputBytesPerRequest` limit.",
|
"description": "Sends data to a process's stdin (pipe mode) or PTY writer (tty mode).\nData can be encoded as base64, utf8, or text. Returns 413 if the decoded\npayload exceeds the configured `maxInputBytesPerRequest` limit.",
|
||||||
"operationId": "post_v1_process_input",
|
"operationId": "post_v1_process_input",
|
||||||
|
|
@ -1367,9 +1307,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/{id}/kill": {
|
"/v1/processes/{id}/kill": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Send SIGKILL to a process.",
|
"summary": "Send SIGKILL to a process.",
|
||||||
"description": "Sends SIGKILL to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.",
|
"description": "Sends SIGKILL to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.",
|
||||||
"operationId": "post_v1_process_kill",
|
"operationId": "post_v1_process_kill",
|
||||||
|
|
@ -1432,9 +1370,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/{id}/logs": {
|
"/v1/processes/{id}/logs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Fetch process logs.",
|
"summary": "Fetch process logs.",
|
||||||
"description": "Returns buffered log entries for a process. Supports filtering by stream\ntype, tail count, and sequence-based resumption. When `follow=true`,\nreturns an SSE stream that replays buffered entries then streams live output.",
|
"description": "Returns buffered log entries for a process. Supports filtering by stream\ntype, tail count, and sequence-based resumption. When `follow=true`,\nreturns an SSE stream that replays buffered entries then streams live output.",
|
||||||
"operationId": "get_v1_process_logs",
|
"operationId": "get_v1_process_logs",
|
||||||
|
|
@ -1532,9 +1468,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/{id}/stop": {
|
"/v1/processes/{id}/stop": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Send SIGTERM to a process.",
|
"summary": "Send SIGTERM to a process.",
|
||||||
"description": "Sends SIGTERM to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.",
|
"description": "Sends SIGTERM to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.",
|
||||||
"operationId": "post_v1_process_stop",
|
"operationId": "post_v1_process_stop",
|
||||||
|
|
@ -1597,9 +1531,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/{id}/terminal/resize": {
|
"/v1/processes/{id}/terminal/resize": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Resize a process terminal.",
|
"summary": "Resize a process terminal.",
|
||||||
"description": "Sets the PTY window size (columns and rows) for a tty-mode process and\nsends SIGWINCH so the child process can adapt.",
|
"description": "Sets the PTY window size (columns and rows) for a tty-mode process and\nsends SIGWINCH so the child process can adapt.",
|
||||||
"operationId": "post_v1_process_terminal_resize",
|
"operationId": "post_v1_process_terminal_resize",
|
||||||
|
|
@ -1680,9 +1612,7 @@
|
||||||
},
|
},
|
||||||
"/v1/processes/{id}/terminal/ws": {
|
"/v1/processes/{id}/terminal/ws": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": ["v1"],
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Open an interactive WebSocket terminal session.",
|
"summary": "Open an interactive WebSocket terminal session.",
|
||||||
"description": "Upgrades the connection to a WebSocket for bidirectional PTY I/O. Accepts\n`access_token` query param for browser-based auth (WebSocket API cannot\nsend custom headers). Streams raw PTY output as binary frames and accepts\nJSON control frames for input, resize, and close.",
|
"description": "Upgrades the connection to a WebSocket for bidirectional PTY I/O. Accepts\n`access_token` query param for browser-based auth (WebSocket API cannot\nsend custom headers). Streams raw PTY output as binary frames and accepts\nJSON control frames for input, resize, and close.",
|
||||||
"operationId": "get_v1_process_terminal_ws",
|
"operationId": "get_v1_process_terminal_ws",
|
||||||
|
|
@ -1759,9 +1689,7 @@
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"AcpEnvelope": {
|
"AcpEnvelope": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["jsonrpc"],
|
||||||
"jsonrpc"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"error": {
|
"error": {
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
|
@ -1795,11 +1723,7 @@
|
||||||
},
|
},
|
||||||
"AcpServerInfo": {
|
"AcpServerInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["serverId", "agent", "createdAtMs"],
|
||||||
"serverId",
|
|
||||||
"agent",
|
|
||||||
"createdAtMs"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"agent": {
|
"agent": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -1815,9 +1739,7 @@
|
||||||
},
|
},
|
||||||
"AcpServerListResponse": {
|
"AcpServerListResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["servers"],
|
||||||
"servers"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"servers": {
|
"servers": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -1908,12 +1830,7 @@
|
||||||
},
|
},
|
||||||
"AgentInfo": {
|
"AgentInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["id", "installed", "credentialsAvailable", "capabilities"],
|
||||||
"id",
|
|
||||||
"installed",
|
|
||||||
"credentialsAvailable",
|
|
||||||
"capabilities"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"$ref": "#/components/schemas/AgentCapabilities"
|
"$ref": "#/components/schemas/AgentCapabilities"
|
||||||
|
|
@ -1956,11 +1873,7 @@
|
||||||
},
|
},
|
||||||
"AgentInstallArtifact": {
|
"AgentInstallArtifact": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["kind", "path", "source"],
|
||||||
"kind",
|
|
||||||
"path",
|
|
||||||
"source"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"kind": {
|
"kind": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -1996,10 +1909,7 @@
|
||||||
},
|
},
|
||||||
"AgentInstallResponse": {
|
"AgentInstallResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["already_installed", "artifacts"],
|
||||||
"already_installed",
|
|
||||||
"artifacts"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"already_installed": {
|
"already_installed": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|
@ -2014,9 +1924,7 @@
|
||||||
},
|
},
|
||||||
"AgentListResponse": {
|
"AgentListResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["agents"],
|
||||||
"agents"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"agents": {
|
"agents": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2049,9 +1957,7 @@
|
||||||
},
|
},
|
||||||
"FsActionResponse": {
|
"FsActionResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["path"],
|
||||||
"path"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"path": {
|
"path": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2060,9 +1966,7 @@
|
||||||
},
|
},
|
||||||
"FsDeleteQuery": {
|
"FsDeleteQuery": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["path"],
|
||||||
"path"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"path": {
|
"path": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2084,12 +1988,7 @@
|
||||||
},
|
},
|
||||||
"FsEntry": {
|
"FsEntry": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["name", "path", "entryType", "size"],
|
||||||
"name",
|
|
||||||
"path",
|
|
||||||
"entryType",
|
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"entryType": {
|
"entryType": {
|
||||||
"$ref": "#/components/schemas/FsEntryType"
|
"$ref": "#/components/schemas/FsEntryType"
|
||||||
|
|
@ -2113,17 +2012,11 @@
|
||||||
},
|
},
|
||||||
"FsEntryType": {
|
"FsEntryType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": ["file", "directory"]
|
||||||
"file",
|
|
||||||
"directory"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"FsMoveRequest": {
|
"FsMoveRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["from", "to"],
|
||||||
"from",
|
|
||||||
"to"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"from": {
|
"from": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2139,10 +2032,7 @@
|
||||||
},
|
},
|
||||||
"FsMoveResponse": {
|
"FsMoveResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["from", "to"],
|
||||||
"from",
|
|
||||||
"to"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"from": {
|
"from": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2154,9 +2044,7 @@
|
||||||
},
|
},
|
||||||
"FsPathQuery": {
|
"FsPathQuery": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["path"],
|
||||||
"path"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"path": {
|
"path": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2165,11 +2053,7 @@
|
||||||
},
|
},
|
||||||
"FsStat": {
|
"FsStat": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["path", "entryType", "size"],
|
||||||
"path",
|
|
||||||
"entryType",
|
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"entryType": {
|
"entryType": {
|
||||||
"$ref": "#/components/schemas/FsEntryType"
|
"$ref": "#/components/schemas/FsEntryType"
|
||||||
|
|
@ -2199,10 +2083,7 @@
|
||||||
},
|
},
|
||||||
"FsUploadBatchResponse": {
|
"FsUploadBatchResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["paths", "truncated"],
|
||||||
"paths",
|
|
||||||
"truncated"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"paths": {
|
"paths": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2217,10 +2098,7 @@
|
||||||
},
|
},
|
||||||
"FsWriteResponse": {
|
"FsWriteResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["path", "bytesWritten"],
|
||||||
"path",
|
|
||||||
"bytesWritten"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"bytesWritten": {
|
"bytesWritten": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -2234,9 +2112,7 @@
|
||||||
},
|
},
|
||||||
"HealthResponse": {
|
"HealthResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["status"],
|
||||||
"status"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2245,10 +2121,7 @@
|
||||||
},
|
},
|
||||||
"McpConfigQuery": {
|
"McpConfigQuery": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["directory", "mcpName"],
|
||||||
"directory",
|
|
||||||
"mcpName"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2262,10 +2135,7 @@
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["command", "type"],
|
||||||
"command",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"args": {
|
"args": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2299,18 +2169,13 @@
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": ["local"]
|
||||||
"local"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["url", "type"],
|
||||||
"url",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"bearerTokenEnvVar": {
|
"bearerTokenEnvVar": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -2358,9 +2223,7 @@
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": ["remote"]
|
||||||
"remote"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2374,11 +2237,7 @@
|
||||||
},
|
},
|
||||||
"ProblemDetails": {
|
"ProblemDetails": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["type", "title", "status"],
|
||||||
"type",
|
|
||||||
"title",
|
|
||||||
"status"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"detail": {
|
"detail": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -2404,14 +2263,7 @@
|
||||||
},
|
},
|
||||||
"ProcessConfig": {
|
"ProcessConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["maxConcurrentProcesses", "defaultRunTimeoutMs", "maxRunTimeoutMs", "maxOutputBytes", "maxLogBytesPerProcess", "maxInputBytesPerRequest"],
|
||||||
"maxConcurrentProcesses",
|
|
||||||
"defaultRunTimeoutMs",
|
|
||||||
"maxRunTimeoutMs",
|
|
||||||
"maxOutputBytes",
|
|
||||||
"maxLogBytesPerProcess",
|
|
||||||
"maxInputBytesPerRequest"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"defaultRunTimeoutMs": {
|
"defaultRunTimeoutMs": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -2443,9 +2295,7 @@
|
||||||
},
|
},
|
||||||
"ProcessCreateRequest": {
|
"ProcessCreateRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["command"],
|
||||||
"command"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"args": {
|
"args": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2476,15 +2326,7 @@
|
||||||
},
|
},
|
||||||
"ProcessInfo": {
|
"ProcessInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["id", "command", "args", "tty", "interactive", "status", "createdAtMs"],
|
||||||
"id",
|
|
||||||
"command",
|
|
||||||
"args",
|
|
||||||
"tty",
|
|
||||||
"interactive",
|
|
||||||
"status",
|
|
||||||
"createdAtMs"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"args": {
|
"args": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2535,9 +2377,7 @@
|
||||||
},
|
},
|
||||||
"ProcessInputRequest": {
|
"ProcessInputRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["data"],
|
||||||
"data"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2550,9 +2390,7 @@
|
||||||
},
|
},
|
||||||
"ProcessInputResponse": {
|
"ProcessInputResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["bytesWritten"],
|
||||||
"bytesWritten"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"bytesWritten": {
|
"bytesWritten": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -2562,9 +2400,7 @@
|
||||||
},
|
},
|
||||||
"ProcessListResponse": {
|
"ProcessListResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["processes"],
|
||||||
"processes"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"processes": {
|
"processes": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2576,13 +2412,7 @@
|
||||||
},
|
},
|
||||||
"ProcessLogEntry": {
|
"ProcessLogEntry": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["sequence", "stream", "timestampMs", "data", "encoding"],
|
||||||
"sequence",
|
|
||||||
"stream",
|
|
||||||
"timestampMs",
|
|
||||||
"data",
|
|
||||||
"encoding"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2634,11 +2464,7 @@
|
||||||
},
|
},
|
||||||
"ProcessLogsResponse": {
|
"ProcessLogsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["processId", "stream", "entries"],
|
||||||
"processId",
|
|
||||||
"stream",
|
|
||||||
"entries"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"entries": {
|
"entries": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2656,18 +2482,11 @@
|
||||||
},
|
},
|
||||||
"ProcessLogsStream": {
|
"ProcessLogsStream": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": ["stdout", "stderr", "combined", "pty"]
|
||||||
"stdout",
|
|
||||||
"stderr",
|
|
||||||
"combined",
|
|
||||||
"pty"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"ProcessRunRequest": {
|
"ProcessRunRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["command"],
|
||||||
"command"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"args": {
|
"args": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2703,14 +2522,7 @@
|
||||||
},
|
},
|
||||||
"ProcessRunResponse": {
|
"ProcessRunResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["timedOut", "stdout", "stderr", "stdoutTruncated", "stderrTruncated", "durationMs"],
|
||||||
"timedOut",
|
|
||||||
"stdout",
|
|
||||||
"stderr",
|
|
||||||
"stdoutTruncated",
|
|
||||||
"stderrTruncated",
|
|
||||||
"durationMs"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"durationMs": {
|
"durationMs": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -2752,17 +2564,11 @@
|
||||||
},
|
},
|
||||||
"ProcessState": {
|
"ProcessState": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": ["running", "exited"]
|
||||||
"running",
|
|
||||||
"exited"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"ProcessTerminalResizeRequest": {
|
"ProcessTerminalResizeRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["cols", "rows"],
|
||||||
"cols",
|
|
||||||
"rows"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"cols": {
|
"cols": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -2778,10 +2584,7 @@
|
||||||
},
|
},
|
||||||
"ProcessTerminalResizeResponse": {
|
"ProcessTerminalResizeResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["cols", "rows"],
|
||||||
"cols",
|
|
||||||
"rows"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"cols": {
|
"cols": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -2797,16 +2600,11 @@
|
||||||
},
|
},
|
||||||
"ServerStatus": {
|
"ServerStatus": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": ["running", "stopped"]
|
||||||
"running",
|
|
||||||
"stopped"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"ServerStatusInfo": {
|
"ServerStatusInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["status"],
|
||||||
"status"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"status": {
|
||||||
"$ref": "#/components/schemas/ServerStatus"
|
"$ref": "#/components/schemas/ServerStatus"
|
||||||
|
|
@ -2821,10 +2619,7 @@
|
||||||
},
|
},
|
||||||
"SkillSource": {
|
"SkillSource": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["type", "source"],
|
||||||
"type",
|
|
||||||
"source"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"ref": {
|
"ref": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -2851,9 +2646,7 @@
|
||||||
},
|
},
|
||||||
"SkillsConfig": {
|
"SkillsConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["sources"],
|
||||||
"sources"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"sources": {
|
"sources": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
@ -2865,10 +2658,7 @@
|
||||||
},
|
},
|
||||||
"SkillsConfigQuery": {
|
"SkillsConfigQuery": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["directory", "skillName"],
|
||||||
"directory",
|
|
||||||
"skillName"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"directory": {
|
"directory": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -2886,4 +2676,4 @@
|
||||||
"description": "ACP proxy v1 API"
|
"description": "ACP proxy v1 API"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,14 @@ setupImage();
|
||||||
|
|
||||||
console.log("Creating BoxLite sandbox...");
|
console.log("Creating BoxLite sandbox...");
|
||||||
const box = new SimpleBox({
|
const box = new SimpleBox({
|
||||||
rootfsPath: OCI_DIR,
|
rootfsPath: OCI_DIR,
|
||||||
env,
|
env,
|
||||||
ports: [{ hostPort: 3000, guestPort: 3000 }],
|
ports: [{ hostPort: 3000, guestPort: 3000 }],
|
||||||
diskSizeGb: 4,
|
diskSizeGb: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Starting server...");
|
console.log("Starting server...");
|
||||||
const result = await box.exec(
|
const result = await box.exec("sh", "-c", "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
|
||||||
"sh", "-c",
|
|
||||||
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
|
|
||||||
);
|
|
||||||
if (result.exitCode !== 0) throw new Error(`Failed to start server: ${result.stderr}`);
|
if (result.exitCode !== 0) throw new Error(`Failed to start server: ${result.stderr}`);
|
||||||
|
|
||||||
const baseUrl = "http://localhost:3000";
|
const baseUrl = "http://localhost:3000";
|
||||||
|
|
@ -36,9 +33,9 @@ console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
clearInterval(keepAlive);
|
clearInterval(keepAlive);
|
||||||
await box.stop();
|
await box.stop();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
process.once("SIGTERM", cleanup);
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ export const DOCKER_IMAGE = "sandbox-agent-boxlite";
|
||||||
export const OCI_DIR = new URL("../oci-image", import.meta.url).pathname;
|
export const OCI_DIR = new URL("../oci-image", import.meta.url).pathname;
|
||||||
|
|
||||||
export function setupImage() {
|
export function setupImage() {
|
||||||
console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`);
|
console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`);
|
||||||
execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" });
|
execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" });
|
||||||
|
|
||||||
if (!existsSync(`${OCI_DIR}/oci-layout`)) {
|
if (!existsSync(`${OCI_DIR}/oci-layout`)) {
|
||||||
console.log("Exporting to OCI layout...");
|
console.log("Exporting to OCI layout...");
|
||||||
mkdirSync(OCI_DIR, { recursive: true });
|
mkdirSync(OCI_DIR, { recursive: true });
|
||||||
execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" });
|
execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export function App() {
|
||||||
console.error("Event stream error:", err);
|
console.error("Event stream error:", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[log]
|
[log],
|
||||||
);
|
);
|
||||||
|
|
||||||
const send = useCallback(async () => {
|
const send = useCallback(async () => {
|
||||||
|
|
@ -162,12 +162,7 @@ export function App() {
|
||||||
<div style={styles.connectForm}>
|
<div style={styles.connectForm}>
|
||||||
<label style={styles.label}>
|
<label style={styles.label}>
|
||||||
Sandbox name:
|
Sandbox name:
|
||||||
<input
|
<input style={styles.input} value={sandboxName} onChange={(e) => setSandboxName(e.target.value)} placeholder="demo" />
|
||||||
style={styles.input}
|
|
||||||
value={sandboxName}
|
|
||||||
onChange={(e) => setSandboxName(e.target.value)}
|
|
||||||
placeholder="demo"
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<button style={styles.button} onClick={connect}>
|
<button style={styles.button} onClick={connect}>
|
||||||
Connect
|
Connect
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,5 @@ import { App } from "./App";
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,65 +2,61 @@ import type { Sandbox } from "@cloudflare/sandbox";
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
|
|
||||||
export type PromptRequest = {
|
export type PromptRequest = {
|
||||||
agent?: string;
|
agent?: string;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function runPromptEndpointStream(
|
export async function runPromptEndpointStream(
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
request: PromptRequest,
|
request: PromptRequest,
|
||||||
port: number,
|
port: number,
|
||||||
emit: (event: { type: string; [key: string]: unknown }) => Promise<void> | void,
|
emit: (event: { type: string; [key: string]: unknown }) => Promise<void> | void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const client = await SandboxAgent.connect({
|
const client = await SandboxAgent.connect({
|
||||||
fetch: (req, init) =>
|
fetch: (req, init) =>
|
||||||
sandbox.containerFetch(
|
sandbox.containerFetch(
|
||||||
req,
|
req,
|
||||||
{
|
{
|
||||||
...(init ?? {}),
|
...(init ?? {}),
|
||||||
// Cloudflare containerFetch may drop long-lived update streams when
|
// Cloudflare containerFetch may drop long-lived update streams when
|
||||||
// a forwarded AbortSignal is cancelled; clear it for this path.
|
// a forwarded AbortSignal is cancelled; clear it for this path.
|
||||||
signal: undefined,
|
signal: undefined,
|
||||||
},
|
},
|
||||||
port,
|
port,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
let unsubscribe: (() => void) | undefined;
|
let unsubscribe: (() => void) | undefined;
|
||||||
try {
|
try {
|
||||||
const session = await client.createSession({
|
const session = await client.createSession({
|
||||||
agent: request.agent ?? "codex",
|
agent: request.agent ?? "codex",
|
||||||
});
|
});
|
||||||
|
|
||||||
const promptText =
|
const promptText = request.prompt?.trim() || "Reply with a short confirmation.";
|
||||||
request.prompt?.trim() || "Reply with a short confirmation.";
|
await emit({
|
||||||
await emit({
|
type: "session.created",
|
||||||
type: "session.created",
|
sessionId: session.id,
|
||||||
sessionId: session.id,
|
agent: session.agent,
|
||||||
agent: session.agent,
|
prompt: promptText,
|
||||||
prompt: promptText,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let pendingWrites: Promise<void> = Promise.resolve();
|
let pendingWrites: Promise<void> = Promise.resolve();
|
||||||
unsubscribe = session.onEvent((event) => {
|
unsubscribe = session.onEvent((event) => {
|
||||||
pendingWrites = pendingWrites
|
pendingWrites = pendingWrites
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await emit({ type: "session.event", event });
|
await emit({ type: "session.event", event });
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await session.prompt([{ type: "text", text: promptText }]);
|
const response = await session.prompt([{ type: "text", text: promptText }]);
|
||||||
await pendingWrites;
|
await pendingWrites;
|
||||||
await emit({ type: "prompt.response", response });
|
await emit({ type: "prompt.response", response });
|
||||||
await emit({ type: "prompt.completed" });
|
await emit({ type: "prompt.completed" });
|
||||||
} finally {
|
} finally {
|
||||||
if (unsubscribe) {
|
if (unsubscribe) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
}
|
}
|
||||||
await Promise.race([
|
await Promise.race([client.dispose(), new Promise((resolve) => setTimeout(resolve, 250))]);
|
||||||
client.dispose(),
|
}
|
||||||
new Promise((resolve) => setTimeout(resolve, 250)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ import { fileURLToPath } from "node:url";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
const REQUEST_TIMEOUT_MS =
|
const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.COMPUTESDK_TIMEOUT_MS || "", 10) || 120_000;
|
||||||
Number.parseInt(process.env.COMPUTESDK_TIMEOUT_MS || "", 10) || 120_000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects and validates the provider to use.
|
* Detects and validates the provider to use.
|
||||||
|
|
@ -24,28 +23,22 @@ const REQUEST_TIMEOUT_MS =
|
||||||
*/
|
*/
|
||||||
function resolveProvider(): ProviderName {
|
function resolveProvider(): ProviderName {
|
||||||
const providerOverride = process.env.COMPUTESDK_PROVIDER;
|
const providerOverride = process.env.COMPUTESDK_PROVIDER;
|
||||||
|
|
||||||
if (providerOverride) {
|
if (providerOverride) {
|
||||||
if (!isValidProvider(providerOverride)) {
|
if (!isValidProvider(providerOverride)) {
|
||||||
throw new Error(
|
throw new Error(`Unsupported ComputeSDK provider "${providerOverride}". Supported providers: ${PROVIDER_NAMES.join(", ")}`);
|
||||||
`Unsupported ComputeSDK provider "${providerOverride}". Supported providers: ${PROVIDER_NAMES.join(", ")}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!isProviderAuthComplete(providerOverride)) {
|
if (!isProviderAuthComplete(providerOverride)) {
|
||||||
const missing = getMissingEnvVars(providerOverride);
|
const missing = getMissingEnvVars(providerOverride);
|
||||||
throw new Error(
|
throw new Error(`Missing credentials for provider "${providerOverride}". Set: ${missing.join(", ")}`);
|
||||||
`Missing credentials for provider "${providerOverride}". Set: ${missing.join(", ")}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
console.log(`Using ComputeSDK provider: ${providerOverride} (explicit)`);
|
console.log(`Using ComputeSDK provider: ${providerOverride} (explicit)`);
|
||||||
return providerOverride as ProviderName;
|
return providerOverride as ProviderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const detected = detectProvider();
|
const detected = detectProvider();
|
||||||
if (!detected) {
|
if (!detected) {
|
||||||
throw new Error(
|
throw new Error(`No provider credentials found. Set one of: ${PROVIDER_NAMES.map((p) => getMissingEnvVars(p).join(", ")).join(" | ")}`);
|
||||||
`No provider credentials found. Set one of: ${PROVIDER_NAMES.map((p) => getMissingEnvVars(p).join(", ")).join(" | ")}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
console.log(`Using ComputeSDK provider: ${detected} (auto-detected)`);
|
console.log(`Using ComputeSDK provider: ${detected} (auto-detected)`);
|
||||||
return detected as ProviderName;
|
return detected as ProviderName;
|
||||||
|
|
@ -53,20 +46,19 @@ function resolveProvider(): ProviderName {
|
||||||
|
|
||||||
function configureComputeSDK(): void {
|
function configureComputeSDK(): void {
|
||||||
const provider = resolveProvider();
|
const provider = resolveProvider();
|
||||||
|
|
||||||
const config: ExplicitComputeConfig = {
|
const config: ExplicitComputeConfig = {
|
||||||
provider,
|
provider,
|
||||||
computesdkApiKey: process.env.COMPUTESDK_API_KEY,
|
computesdkApiKey: process.env.COMPUTESDK_API_KEY,
|
||||||
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const providerConfig = getProviderConfigFromEnv(provider);
|
const providerConfig = getProviderConfigFromEnv(provider);
|
||||||
if (Object.keys(providerConfig).length > 0) {
|
if (Object.keys(providerConfig).length > 0) {
|
||||||
const configWithProvider =
|
const configWithProvider = config as ExplicitComputeConfig & Record<ProviderName, Record<string, string>>;
|
||||||
config as ExplicitComputeConfig & Record<ProviderName, Record<string, string>>;
|
|
||||||
configWithProvider[provider] = providerConfig;
|
configWithProvider[provider] = providerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
compute.setConfig(config);
|
compute.setConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,9 +141,7 @@ export async function runComputeSdkExample(): Promise<void> {
|
||||||
await new Promise(() => {});
|
await new Promise(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDirectRun = Boolean(
|
const isDirectRun = Boolean(process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url));
|
||||||
process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDirectRun) {
|
if (isDirectRun) {
|
||||||
runComputeSdkExample().catch((error) => {
|
runComputeSdkExample().catch((error) => {
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,7 @@ import { setupComputeSdkSandboxAgent } from "../src/computesdk.ts";
|
||||||
const hasModal = Boolean(process.env.MODAL_TOKEN_ID && process.env.MODAL_TOKEN_SECRET);
|
const hasModal = Boolean(process.env.MODAL_TOKEN_ID && process.env.MODAL_TOKEN_SECRET);
|
||||||
const hasVercel = Boolean(process.env.VERCEL_TOKEN || process.env.VERCEL_OIDC_TOKEN);
|
const hasVercel = Boolean(process.env.VERCEL_TOKEN || process.env.VERCEL_OIDC_TOKEN);
|
||||||
const hasProviderKey = Boolean(
|
const hasProviderKey = Boolean(
|
||||||
process.env.BLAXEL_API_KEY ||
|
process.env.BLAXEL_API_KEY || process.env.CSB_API_KEY || process.env.DAYTONA_API_KEY || process.env.E2B_API_KEY || hasModal || hasVercel,
|
||||||
process.env.CSB_API_KEY ||
|
|
||||||
process.env.DAYTONA_API_KEY ||
|
|
||||||
process.env.E2B_API_KEY ||
|
|
||||||
hasModal ||
|
|
||||||
hasVercel
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldRun = Boolean(process.env.COMPUTESDK_API_KEY) && hasProviderKey;
|
const shouldRun = Boolean(process.env.COMPUTESDK_API_KEY) && hasProviderKey;
|
||||||
|
|
@ -34,6 +29,6 @@ describe("computesdk example", () => {
|
||||||
await cleanup();
|
await cleanup();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeoutMs
|
timeoutMs,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,19 @@ import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
||||||
const daytona = new Daytona();
|
const daytona = new Daytona();
|
||||||
|
|
||||||
const envVars: Record<string, string> = {};
|
const envVars: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY)
|
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||||
if (process.env.OPENAI_API_KEY)
|
|
||||||
envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
||||||
|
|
||||||
// Build a custom image with sandbox-agent pre-installed (slower first run, faster subsequent runs)
|
// Build a custom image with sandbox-agent pre-installed (slower first run, faster subsequent runs)
|
||||||
const image = Image.base("ubuntu:22.04").runCommands(
|
const image = Image.base("ubuntu:22.04").runCommands(
|
||||||
"apt-get update && apt-get install -y curl ca-certificates",
|
"apt-get update && apt-get install -y curl ca-certificates",
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("Creating Daytona sandbox (first run builds the base image and may take a few minutes, subsequent runs are fast)...");
|
console.log("Creating Daytona sandbox (first run builds the base image and may take a few minutes, subsequent runs are fast)...");
|
||||||
const sandbox = await daytona.create({ envVars, image, autoStopInterval: 0 }, { timeout: 180 });
|
const sandbox = await daytona.create({ envVars, image, autoStopInterval: 0 }, { timeout: 180 });
|
||||||
|
|
||||||
await sandbox.process.executeCommand(
|
await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
|
||||||
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
|
|
||||||
);
|
|
||||||
|
|
||||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||||
|
|
||||||
|
|
@ -35,9 +31,9 @@ console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
clearInterval(keepAlive);
|
clearInterval(keepAlive);
|
||||||
await sandbox.delete(60);
|
await sandbox.delete(60);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
process.once("SIGTERM", cleanup);
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@ import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
||||||
const daytona = new Daytona();
|
const daytona = new Daytona();
|
||||||
|
|
||||||
const envVars: Record<string, string> = {};
|
const envVars: Record<string, string> = {};
|
||||||
if (process.env.ANTHROPIC_API_KEY)
|
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||||
envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||||
if (process.env.OPENAI_API_KEY)
|
|
||||||
envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
||||||
|
|
||||||
// Use default image and install sandbox-agent at runtime (faster startup, no snapshot build)
|
// Use default image and install sandbox-agent at runtime (faster startup, no snapshot build)
|
||||||
console.log("Creating Daytona sandbox...");
|
console.log("Creating Daytona sandbox...");
|
||||||
|
|
@ -16,17 +14,13 @@ const sandbox = await daytona.create({ envVars, autoStopInterval: 0 });
|
||||||
|
|
||||||
// Install sandbox-agent and start server
|
// Install sandbox-agent and start server
|
||||||
console.log("Installing sandbox-agent...");
|
console.log("Installing sandbox-agent...");
|
||||||
await sandbox.process.executeCommand(
|
await sandbox.process.executeCommand("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh");
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Installing agents...");
|
console.log("Installing agents...");
|
||||||
await sandbox.process.executeCommand("sandbox-agent install-agent claude");
|
await sandbox.process.executeCommand("sandbox-agent install-agent claude");
|
||||||
await sandbox.process.executeCommand("sandbox-agent install-agent codex");
|
await sandbox.process.executeCommand("sandbox-agent install-agent codex");
|
||||||
|
|
||||||
await sandbox.process.executeCommand(
|
await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
|
||||||
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
|
|
||||||
);
|
|
||||||
|
|
||||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
||||||
|
|
||||||
|
|
@ -40,9 +34,9 @@ console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
clearInterval(keepAlive);
|
clearInterval(keepAlive);
|
||||||
await sandbox.delete(60);
|
await sandbox.delete(60);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
process.once("SIGTERM", cleanup);
|
process.once("SIGTERM", cleanup);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,6 @@ describe("daytona example", () => {
|
||||||
await cleanup();
|
await cleanup();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeoutMs
|
timeoutMs,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@ const IMAGE = "node:22-bookworm-slim";
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
const agent = detectAgent();
|
const agent = detectAgent();
|
||||||
const codexAuthPath = process.env.HOME ? path.join(process.env.HOME, ".codex", "auth.json") : null;
|
const codexAuthPath = process.env.HOME ? path.join(process.env.HOME, ".codex", "auth.json") : null;
|
||||||
const bindMounts = codexAuthPath && fs.existsSync(codexAuthPath)
|
const bindMounts = codexAuthPath && fs.existsSync(codexAuthPath) ? [`${codexAuthPath}:/root/.codex/auth.json:ro`] : [];
|
||||||
? [`${codexAuthPath}:/root/.codex/auth.json:ro`]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
||||||
|
|
||||||
|
|
@ -22,7 +20,7 @@ try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
docker.modem.followProgress(stream, (err: Error | null) => err ? reject(err) : resolve());
|
docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -30,13 +28,17 @@ try {
|
||||||
console.log("Starting container...");
|
console.log("Starting container...");
|
||||||
const container = await docker.createContainer({
|
const container = await docker.createContainer({
|
||||||
Image: IMAGE,
|
Image: IMAGE,
|
||||||
Cmd: ["sh", "-c", [
|
Cmd: [
|
||||||
"apt-get update",
|
"sh",
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6",
|
"-c",
|
||||||
"rm -rf /var/lib/apt/lists/*",
|
[
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
"apt-get update",
|
||||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6",
|
||||||
].join(" && ")],
|
"rm -rf /var/lib/apt/lists/*",
|
||||||
|
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
||||||
|
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
||||||
|
].join(" && "),
|
||||||
|
],
|
||||||
Env: [
|
Env: [
|
||||||
process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "",
|
process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "",
|
||||||
process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "",
|
process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "",
|
||||||
|
|
@ -63,8 +65,12 @@ console.log(" Press Ctrl+C to stop.");
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
clearInterval(keepAlive);
|
clearInterval(keepAlive);
|
||||||
try { await container.stop({ t: 5 }); } catch {}
|
try {
|
||||||
try { await container.remove({ force: true }); } catch {}
|
await container.stop({ t: 5 });
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
await container.remove({ force: true });
|
||||||
|
} catch {}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,6 @@ describe("docker example", () => {
|
||||||
await cleanup();
|
await cleanup();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeoutMs
|
timeoutMs,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,6 @@ describe("e2b example", () => {
|
||||||
await cleanup();
|
await cleanup();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeoutMs
|
timeoutMs,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,7 @@ console.log("Uploading files via batch tar...");
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
const tarPath = path.join(tmpDir, "upload.tar");
|
const tarPath = path.join(tmpDir, "upload.tar");
|
||||||
await tar.create(
|
await tar.create({ file: tarPath, cwd: tmpDir }, ["my-project"]);
|
||||||
{ file: tarPath, cwd: tmpDir },
|
|
||||||
["my-project"],
|
|
||||||
);
|
|
||||||
const tarBuffer = await fs.promises.readFile(tarPath);
|
const tarBuffer = await fs.promises.readFile(tarPath);
|
||||||
const uploadResult = await client.uploadFsBatch(tarBuffer, { path: "/opt" });
|
const uploadResult = await client.uploadFsBatch(tarBuffer, { path: "/opt" });
|
||||||
console.log(` Uploaded ${uploadResult.paths.length} files: ${uploadResult.paths.join(", ")}`);
|
console.log(` Uploaded ${uploadResult.paths.length} files: ${uploadResult.paths.join(", ")}`);
|
||||||
|
|
@ -54,4 +51,7 @@ console.log(' Try: "read the README in /opt/my-project"');
|
||||||
console.log(" Press Ctrl+C to stop.");
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
process.on("SIGINT", () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
|
cleanup().then(() => process.exit(0));
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,7 @@ console.log("Uploading MCP server bundle...");
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
const bundle = await fs.promises.readFile(serverFile);
|
const bundle = await fs.promises.readFile(serverFile);
|
||||||
const written = await client.writeFsFile(
|
const written = await client.writeFsFile({ path: "/opt/mcp/custom-tools/mcp-server.cjs" }, bundle);
|
||||||
{ path: "/opt/mcp/custom-tools/mcp-server.cjs" },
|
|
||||||
bundle,
|
|
||||||
);
|
|
||||||
console.log(` Written: ${written.path} (${written.bytesWritten} bytes)`);
|
console.log(` Written: ${written.path} (${written.bytesWritten} bytes)`);
|
||||||
|
|
||||||
// Create a session with the uploaded MCP server as a local command.
|
// Create a session with the uploaded MCP server as a local command.
|
||||||
|
|
@ -35,12 +32,14 @@ const session = await client.createSession({
|
||||||
agent: detectAgent(),
|
agent: detectAgent(),
|
||||||
sessionInit: {
|
sessionInit: {
|
||||||
cwd: "/root",
|
cwd: "/root",
|
||||||
mcpServers: [{
|
mcpServers: [
|
||||||
name: "customTools",
|
{
|
||||||
command: "node",
|
name: "customTools",
|
||||||
args: ["/opt/mcp/custom-tools/mcp-server.cjs"],
|
command: "node",
|
||||||
env: [],
|
args: ["/opt/mcp/custom-tools/mcp-server.cjs"],
|
||||||
}],
|
env: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const sessionId = session.id;
|
const sessionId = session.id;
|
||||||
|
|
@ -49,4 +48,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
|
||||||
console.log(" Press Ctrl+C to stop.");
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
process.on("SIGINT", () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
|
cleanup().then(() => process.exit(0));
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||||
console.log("Starting sandbox...");
|
console.log("Starting sandbox...");
|
||||||
const { baseUrl, cleanup } = await startDockerSandbox({
|
const { baseUrl, cleanup } = await startDockerSandbox({
|
||||||
port: 3002,
|
port: 3002,
|
||||||
setupCommands: [
|
setupCommands: ["npm install -g --silent @modelcontextprotocol/server-everything@2026.1.26"],
|
||||||
"npm install -g --silent @modelcontextprotocol/server-everything@2026.1.26",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Creating session with everything MCP server...");
|
console.log("Creating session with everything MCP server...");
|
||||||
|
|
@ -16,12 +14,14 @@ const session = await client.createSession({
|
||||||
agent: detectAgent(),
|
agent: detectAgent(),
|
||||||
sessionInit: {
|
sessionInit: {
|
||||||
cwd: "/root",
|
cwd: "/root",
|
||||||
mcpServers: [{
|
mcpServers: [
|
||||||
name: "everything",
|
{
|
||||||
command: "mcp-server-everything",
|
name: "everything",
|
||||||
args: [],
|
command: "mcp-server-everything",
|
||||||
env: [],
|
args: [],
|
||||||
}],
|
env: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const sessionId = session.id;
|
const sessionId = session.id;
|
||||||
|
|
@ -30,4 +30,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
|
||||||
console.log(" Press Ctrl+C to stop.");
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
process.on("SIGINT", () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
|
cleanup().then(() => process.exit(0));
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
import { createInterface } from "node:readline/promises";
|
import { createInterface } from "node:readline/promises";
|
||||||
import { stdin as input, stdout as output } from "node:process";
|
import { stdin as input, stdout as output } from "node:process";
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import {
|
import { SandboxAgent, type PermissionReply, type SessionPermissionRequest } from "sandbox-agent";
|
||||||
SandboxAgent,
|
|
||||||
type PermissionReply,
|
|
||||||
type SessionPermissionRequest,
|
|
||||||
} from "sandbox-agent";
|
|
||||||
|
|
||||||
const options = parseOptions();
|
const options = parseOptions();
|
||||||
const agent = options.agent.trim().toLowerCase();
|
const agent = options.agent.trim().toLowerCase();
|
||||||
const autoReply = parsePermissionReply(options.reply);
|
const autoReply = parsePermissionReply(options.reply);
|
||||||
const promptText =
|
const promptText = options.prompt?.trim() || `Create ./permission-example.txt with the text 'hello from the ${agent} permissions example'.`;
|
||||||
options.prompt?.trim() ||
|
|
||||||
`Create ./permission-example.txt with the text 'hello from the ${agent} permissions example'.`;
|
|
||||||
|
|
||||||
const sdk = await SandboxAgent.start({
|
const sdk = await SandboxAgent.start({
|
||||||
spawn: {
|
spawn: {
|
||||||
|
|
@ -31,11 +25,7 @@ try {
|
||||||
: [];
|
: [];
|
||||||
const modeOption = configOptions.find((option) => option.category === "mode");
|
const modeOption = configOptions.find((option) => option.category === "mode");
|
||||||
const availableModes = extractOptionValues(modeOption);
|
const availableModes = extractOptionValues(modeOption);
|
||||||
const mode =
|
const mode = options.mode?.trim() || (typeof modeOption?.currentValue === "string" ? modeOption.currentValue : "") || availableModes[0] || "";
|
||||||
options.mode?.trim() ||
|
|
||||||
(typeof modeOption?.currentValue === "string" ? modeOption.currentValue : "") ||
|
|
||||||
availableModes[0] ||
|
|
||||||
"";
|
|
||||||
|
|
||||||
console.log(`Agent: ${agent}`);
|
console.log(`Agent: ${agent}`);
|
||||||
console.log(`Mode: ${mode || "(default)"}`);
|
console.log(`Mode: ${mode || "(default)"}`);
|
||||||
|
|
@ -91,10 +81,7 @@ async function handlePermissionRequest(
|
||||||
await session.respondPermission(request.id, reply);
|
await session.respondPermission(request.id, reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promptForReply(
|
async function promptForReply(request: SessionPermissionRequest, rl: ReturnType<typeof createInterface> | null): Promise<PermissionReply> {
|
||||||
request: SessionPermissionRequest,
|
|
||||||
rl: ReturnType<typeof createInterface> | null,
|
|
||||||
): Promise<PermissionReply> {
|
|
||||||
if (!rl) {
|
if (!rl) {
|
||||||
return "reject";
|
return "reject";
|
||||||
}
|
}
|
||||||
|
|
@ -136,8 +123,7 @@ function extractOptionValues(option: { options?: unknown[] } | undefined): strin
|
||||||
if (!nested || typeof nested !== "object") {
|
if (!nested || typeof nested !== "object") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const nestedValue =
|
const nestedValue = "value" in nested && typeof nested.value === "string" ? nested.value : null;
|
||||||
"value" in nested && typeof nested.value === "string" ? nested.value : null;
|
|
||||||
if (nestedValue) {
|
if (nestedValue) {
|
||||||
values.push(nestedValue);
|
values.push(nestedValue);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@ const persist = new InMemorySessionPersistDriver();
|
||||||
console.log("Starting sandbox...");
|
console.log("Starting sandbox...");
|
||||||
const sandbox = await startDockerSandbox({
|
const sandbox = await startDockerSandbox({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
setupCommands: [
|
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
||||||
"sandbox-agent install-agent claude",
|
|
||||||
"sandbox-agent install-agent codex",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,47 @@ if (process.env.DATABASE_URL) {
|
||||||
connectionString = process.env.DATABASE_URL;
|
connectionString = process.env.DATABASE_URL;
|
||||||
} else {
|
} else {
|
||||||
const name = `persist-example-${randomUUID().slice(0, 8)}`;
|
const name = `persist-example-${randomUUID().slice(0, 8)}`;
|
||||||
containerId = execFileSync("docker", [
|
containerId = execFileSync(
|
||||||
"run", "-d", "--rm", "--name", name,
|
"docker",
|
||||||
"-e", "POSTGRES_USER=postgres", "-e", "POSTGRES_PASSWORD=postgres", "-e", "POSTGRES_DB=sandbox",
|
[
|
||||||
"-p", "127.0.0.1::5432", "postgres:16-alpine",
|
"run",
|
||||||
], { encoding: "utf8" }).trim();
|
"-d",
|
||||||
|
"--rm",
|
||||||
|
"--name",
|
||||||
|
name,
|
||||||
|
"-e",
|
||||||
|
"POSTGRES_USER=postgres",
|
||||||
|
"-e",
|
||||||
|
"POSTGRES_PASSWORD=postgres",
|
||||||
|
"-e",
|
||||||
|
"POSTGRES_DB=sandbox",
|
||||||
|
"-p",
|
||||||
|
"127.0.0.1::5432",
|
||||||
|
"postgres:16-alpine",
|
||||||
|
],
|
||||||
|
{ encoding: "utf8" },
|
||||||
|
).trim();
|
||||||
const port = execFileSync("docker", ["port", containerId, "5432/tcp"], { encoding: "utf8" })
|
const port = execFileSync("docker", ["port", containerId, "5432/tcp"], { encoding: "utf8" })
|
||||||
.trim().split("\n")[0]?.match(/:(\d+)$/)?.[1];
|
.trim()
|
||||||
|
.split("\n")[0]
|
||||||
|
?.match(/:(\d+)$/)?.[1];
|
||||||
connectionString = `postgres://postgres:postgres@127.0.0.1:${port}/sandbox`;
|
connectionString = `postgres://postgres:postgres@127.0.0.1:${port}/sandbox`;
|
||||||
console.log(`Postgres on port ${port}`);
|
console.log(`Postgres on port ${port}`);
|
||||||
|
|
||||||
const deadline = Date.now() + 30_000;
|
const deadline = Date.now() + 30_000;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const c = new Client({ connectionString });
|
const c = new Client({ connectionString });
|
||||||
try { await c.connect(); await c.query("SELECT 1"); await c.end(); break; }
|
try {
|
||||||
catch { try { await c.end(); } catch {} await delay(250); }
|
await c.connect();
|
||||||
|
await c.query("SELECT 1");
|
||||||
|
await c.end();
|
||||||
|
break;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await c.end();
|
||||||
|
} catch {}
|
||||||
|
await delay(250);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,10 +66,7 @@ try {
|
||||||
console.log("Starting sandbox...");
|
console.log("Starting sandbox...");
|
||||||
const sandbox = await startDockerSandbox({
|
const sandbox = await startDockerSandbox({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
setupCommands: [
|
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
||||||
"sandbox-agent install-agent claude",
|
|
||||||
"sandbox-agent install-agent codex",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||||
|
|
@ -71,6 +94,8 @@ try {
|
||||||
await sandbox.cleanup();
|
await sandbox.cleanup();
|
||||||
} finally {
|
} finally {
|
||||||
if (containerId) {
|
if (containerId) {
|
||||||
try { execFileSync("docker", ["rm", "-f", containerId], { stdio: "ignore" }); } catch {}
|
try {
|
||||||
|
execFileSync("docker", ["rm", "-f", containerId], { stdio: "ignore" });
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,7 @@ const persist = new SQLiteSessionPersistDriver({ filename: "./sessions.db" });
|
||||||
console.log("Starting sandbox...");
|
console.log("Starting sandbox...");
|
||||||
const sandbox = await startDockerSandbox({
|
const sandbox = await startDockerSandbox({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
setupCommands: [
|
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
||||||
"sandbox-agent install-agent claude",
|
|
||||||
"sandbox-agent install-agent codex",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ const DIRECT_CREDENTIAL_KEYS = [
|
||||||
|
|
||||||
function stripShellQuotes(value: string): string {
|
function stripShellQuotes(value: string): string {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
||||||
return trimmed.slice(1, -1);
|
return trimmed.slice(1, -1);
|
||||||
}
|
}
|
||||||
if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
||||||
|
|
@ -107,11 +107,7 @@ function collectCredentialEnv(): Record<string, string> {
|
||||||
const merged: Record<string, string> = {};
|
const merged: Record<string, string> = {};
|
||||||
let extracted: Record<string, string> = {};
|
let extracted: Record<string, string> = {};
|
||||||
try {
|
try {
|
||||||
const output = execFileSync(
|
const output = execFileSync("sandbox-agent", ["credentials", "extract-env"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
||||||
"sandbox-agent",
|
|
||||||
["credentials", "extract-env"],
|
|
||||||
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
||||||
);
|
|
||||||
extracted = parseExtractedCredentials(output);
|
extracted = parseExtractedCredentials(output);
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to direct env vars if extraction is unavailable.
|
// Fall back to direct env vars if extraction is unavailable.
|
||||||
|
|
@ -132,10 +128,7 @@ function shellSingleQuotedLiteral(value: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripAnsi(value: string): string {
|
function stripAnsi(value: string): string {
|
||||||
return value.replace(
|
return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g, "");
|
||||||
/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureExampleImage(_docker: Docker): Promise<string> {
|
async function ensureExampleImage(_docker: Docker): Promise<string> {
|
||||||
|
|
@ -145,11 +138,7 @@ async function ensureExampleImage(_docker: Docker): Promise<string> {
|
||||||
if (dev) {
|
if (dev) {
|
||||||
console.log(" Building sandbox image from source (may take a while, only runs once)...");
|
console.log(" Building sandbox image from source (may take a while, only runs once)...");
|
||||||
try {
|
try {
|
||||||
execFileSync("docker", [
|
execFileSync("docker", ["build", "-t", imageName, "-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"), REPO_ROOT], {
|
||||||
"build", "-t", imageName,
|
|
||||||
"-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"),
|
|
||||||
REPO_ROOT,
|
|
||||||
], {
|
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|
@ -224,19 +213,13 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
||||||
image = await ensureExampleImage(docker);
|
image = await ensureExampleImage(docker);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bootCommands = [
|
const bootCommands = [...setupCommands, `sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`];
|
||||||
...setupCommands,
|
|
||||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
const container = await docker.createContainer({
|
const container = await docker.createContainer({
|
||||||
Image: image,
|
Image: image,
|
||||||
WorkingDir: "/root",
|
WorkingDir: "/root",
|
||||||
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
||||||
Env: [
|
Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)],
|
||||||
...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`),
|
|
||||||
...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`),
|
|
||||||
],
|
|
||||||
ExposedPorts: { [`${port}/tcp`]: {} },
|
ExposedPorts: { [`${port}/tcp`]: {} },
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
|
|
@ -246,12 +229,12 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
||||||
await container.start();
|
await container.start();
|
||||||
|
|
||||||
const logChunks: string[] = [];
|
const logChunks: string[] = [];
|
||||||
const startupLogs = await container.logs({
|
const startupLogs = (await container.logs({
|
||||||
follow: true,
|
follow: true,
|
||||||
stdout: true,
|
stdout: true,
|
||||||
stderr: true,
|
stderr: true,
|
||||||
since: 0,
|
since: 0,
|
||||||
}) as NodeJS.ReadableStream;
|
})) as NodeJS.ReadableStream;
|
||||||
const stdoutStream = new PassThrough();
|
const stdoutStream = new PassThrough();
|
||||||
const stderrStream = new PassThrough();
|
const stderrStream = new PassThrough();
|
||||||
stdoutStream.on("data", (chunk) => {
|
stdoutStream.on("data", (chunk) => {
|
||||||
|
|
@ -263,7 +246,9 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
||||||
docker.modem.demuxStream(startupLogs, stdoutStream, stderrStream);
|
docker.modem.demuxStream(startupLogs, stdoutStream, stderrStream);
|
||||||
const stopStartupLogs = () => {
|
const stopStartupLogs = () => {
|
||||||
const stream = startupLogs as NodeJS.ReadableStream & { destroy?: () => void };
|
const stream = startupLogs as NodeJS.ReadableStream & { destroy?: () => void };
|
||||||
try { stream.destroy?.(); } catch {}
|
try {
|
||||||
|
stream.destroy?.();
|
||||||
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const inspect = await container.inspect();
|
const inspect = await container.inspect();
|
||||||
|
|
@ -279,8 +264,12 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
||||||
|
|
||||||
const cleanup = async () => {
|
const cleanup = async () => {
|
||||||
stopStartupLogs();
|
stopStartupLogs();
|
||||||
try { await container.stop({ t: 5 }); } catch {}
|
try {
|
||||||
try { await container.remove({ force: true }); } catch {}
|
await container.stop({ t: 5 });
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
await container.remove({ force: true });
|
||||||
|
} catch {}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.once("SIGINT", cleanup);
|
process.once("SIGINT", cleanup);
|
||||||
|
|
|
||||||
|
|
@ -41,15 +41,7 @@ export function buildInspectorUrl({
|
||||||
return `${normalized}/ui/${sessionPath}${queryString ? `?${queryString}` : ""}`;
|
return `${normalized}/ui/${sessionPath}${queryString ? `?${queryString}` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logInspectorUrl({
|
export function logInspectorUrl({ baseUrl, token, headers }: { baseUrl: string; token?: string; headers?: Record<string, string> }): void {
|
||||||
baseUrl,
|
|
||||||
token,
|
|
||||||
headers,
|
|
||||||
}: {
|
|
||||||
baseUrl: string;
|
|
||||||
token?: string;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
}): void {
|
|
||||||
console.log(`Inspector: ${buildInspectorUrl({ baseUrl, token, headers })}`);
|
console.log(`Inspector: ${buildInspectorUrl({ baseUrl, token, headers })}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,10 +76,7 @@ export function generateSessionId(): string {
|
||||||
export function detectAgent(): string {
|
export function detectAgent(): string {
|
||||||
if (process.env.SANDBOX_AGENT) return process.env.SANDBOX_AGENT;
|
if (process.env.SANDBOX_AGENT) return process.env.SANDBOX_AGENT;
|
||||||
const hasClaude = Boolean(
|
const hasClaude = Boolean(
|
||||||
process.env.ANTHROPIC_API_KEY ||
|
process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN,
|
||||||
process.env.CLAUDE_API_KEY ||
|
|
||||||
process.env.CLAUDE_CODE_OAUTH_TOKEN ||
|
|
||||||
process.env.ANTHROPIC_AUTH_TOKEN,
|
|
||||||
);
|
);
|
||||||
const openAiLikeKey = process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || "";
|
const openAiLikeKey = process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || "";
|
||||||
const hasCodexApiKey = openAiLikeKey.startsWith("sk-");
|
const hasCodexApiKey = openAiLikeKey.startsWith("sk-");
|
||||||
|
|
|
||||||
|
|
@ -23,25 +23,16 @@ console.log("Uploading script and skill file...");
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
|
|
||||||
const script = await fs.promises.readFile(scriptFile);
|
const script = await fs.promises.readFile(scriptFile);
|
||||||
const scriptResult = await client.writeFsFile(
|
const scriptResult = await client.writeFsFile({ path: "/opt/skills/random-number/random-number.cjs" }, script);
|
||||||
{ path: "/opt/skills/random-number/random-number.cjs" },
|
|
||||||
script,
|
|
||||||
);
|
|
||||||
console.log(` Script: ${scriptResult.path} (${scriptResult.bytesWritten} bytes)`);
|
console.log(` Script: ${scriptResult.path} (${scriptResult.bytesWritten} bytes)`);
|
||||||
|
|
||||||
const skillMd = await fs.promises.readFile(path.resolve(__dirname, "../SKILL.md"));
|
const skillMd = await fs.promises.readFile(path.resolve(__dirname, "../SKILL.md"));
|
||||||
const skillResult = await client.writeFsFile(
|
const skillResult = await client.writeFsFile({ path: "/opt/skills/random-number/SKILL.md" }, skillMd);
|
||||||
{ path: "/opt/skills/random-number/SKILL.md" },
|
|
||||||
skillMd,
|
|
||||||
);
|
|
||||||
console.log(` Skill: ${skillResult.path} (${skillResult.bytesWritten} bytes)`);
|
console.log(` Skill: ${skillResult.path} (${skillResult.bytesWritten} bytes)`);
|
||||||
|
|
||||||
// Configure the uploaded skill.
|
// Configure the uploaded skill.
|
||||||
console.log("Configuring custom skill...");
|
console.log("Configuring custom skill...");
|
||||||
await client.setSkillsConfig(
|
await client.setSkillsConfig({ directory: "/", skillName: "random-number" }, { sources: [{ type: "local", source: "/opt/skills/random-number" }] });
|
||||||
{ directory: "/", skillName: "random-number" },
|
|
||||||
{ sources: [{ type: "local", source: "/opt/skills/random-number" }] },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a session.
|
// Create a session.
|
||||||
console.log("Creating session with custom skill...");
|
console.log("Creating session with custom skill...");
|
||||||
|
|
@ -52,4 +43,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
|
||||||
console.log(" Press Ctrl+C to stop.");
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
process.on("SIGINT", () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
|
cleanup().then(() => process.exit(0));
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,7 @@ console.log(' Try: "How do I start sandbox-agent?"');
|
||||||
console.log(" Press Ctrl+C to stop.");
|
console.log(" Press Ctrl+C to stop.");
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
const keepAlive = setInterval(() => {}, 60_000);
|
||||||
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
|
process.on("SIGINT", () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
|
cleanup().then(() => process.exit(0));
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,6 @@ describe("vercel example", () => {
|
||||||
await cleanup();
|
await cleanup();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeoutMs
|
timeoutMs,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": [
|
"lib": ["ES2022", "DOM"],
|
||||||
"ES2022",
|
|
||||||
"DOM"
|
|
||||||
],
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
|
@ -14,11 +11,6 @@
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*"],
|
||||||
"src/**/*"
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"**/*.test.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
factory/factory-cloud.md
Normal file
12
factory/factory-cloud.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Factory Cloud
|
||||||
|
|
||||||
|
## Mock Server
|
||||||
|
|
||||||
|
If you are running the mock server with Beat instead of `docker compose`, use a team accession for the process so it does not terminate when your message is finished.
|
||||||
|
|
||||||
|
A detached `tmux` session is acceptable for this. Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tmux new-session -d -s mock-ui-4180 \
|
||||||
|
'cd /Users/nathan/conductor/workspaces/sandbox-agent/provo && OPENHANDOFF_FRONTEND_CLIENT_MODE=mock pnpm --filter @openhandoff/frontend exec vite --host localhost --port 4180'
|
||||||
|
```
|
||||||
|
|
@ -94,7 +94,7 @@ async function generateOne(drizzleDir: string): Promise<void> {
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2
|
2,
|
||||||
);
|
);
|
||||||
|
|
||||||
const outPath = resolve(drizzleDir, "..", "migrations.ts");
|
const outPath = resolve(drizzleDir, "..", "migrations.ts");
|
||||||
|
|
@ -128,9 +128,8 @@ async function main(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error: unknown) => {
|
main().catch((error: unknown) => {
|
||||||
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(message);
|
console.error(message);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,7 @@ let providerRegistry: ProviderRegistry | null = null;
|
||||||
let notificationService: NotificationService | null = null;
|
let notificationService: NotificationService | null = null;
|
||||||
let runtimeDriver: BackendDriver | null = null;
|
let runtimeDriver: BackendDriver | null = null;
|
||||||
|
|
||||||
export function initActorRuntimeContext(
|
export function initActorRuntimeContext(config: AppConfig, providers: ProviderRegistry, notifications?: NotificationService, driver?: BackendDriver): void {
|
||||||
config: AppConfig,
|
|
||||||
providers: ProviderRegistry,
|
|
||||||
notifications?: NotificationService,
|
|
||||||
driver?: BackendDriver
|
|
||||||
): void {
|
|
||||||
runtimeConfig = config;
|
runtimeConfig = config;
|
||||||
providerRegistry = providers;
|
providerRegistry = providers;
|
||||||
notificationService = notifications ?? null;
|
notificationService = notifications ?? null;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
import {
|
import { handoffKey, handoffStatusSyncKey, historyKey, projectBranchSyncKey, projectKey, projectPrSyncKey, sandboxInstanceKey, workspaceKey } from "./keys.js";
|
||||||
handoffKey,
|
|
||||||
handoffStatusSyncKey,
|
|
||||||
historyKey,
|
|
||||||
projectBranchSyncKey,
|
|
||||||
projectKey,
|
|
||||||
projectPrSyncKey,
|
|
||||||
sandboxInstanceKey,
|
|
||||||
workspaceKey
|
|
||||||
} from "./keys.js";
|
|
||||||
import type { ProviderId } from "@openhandoff/shared";
|
import type { ProviderId } from "@openhandoff/shared";
|
||||||
|
|
||||||
export function actorClient(c: any) {
|
export function actorClient(c: any) {
|
||||||
|
|
@ -16,7 +7,7 @@ export function actorClient(c: any) {
|
||||||
|
|
||||||
export async function getOrCreateWorkspace(c: any, workspaceId: string) {
|
export async function getOrCreateWorkspace(c: any, workspaceId: string) {
|
||||||
return await actorClient(c).workspace.getOrCreate(workspaceKey(workspaceId), {
|
return await actorClient(c).workspace.getOrCreate(workspaceKey(workspaceId), {
|
||||||
createWithInput: workspaceId
|
createWithInput: workspaceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,8 +16,8 @@ export async function getOrCreateProject(c: any, workspaceId: string, repoId: st
|
||||||
createWithInput: {
|
createWithInput: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
repoId,
|
repoId,
|
||||||
remoteUrl
|
remoteUrl,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,15 +29,9 @@ export function getHandoff(c: any, workspaceId: string, repoId: string, handoffI
|
||||||
return actorClient(c).handoff.get(handoffKey(workspaceId, repoId, handoffId));
|
return actorClient(c).handoff.get(handoffKey(workspaceId, repoId, handoffId));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateHandoff(
|
export async function getOrCreateHandoff(c: any, workspaceId: string, repoId: string, handoffId: string, createWithInput: Record<string, unknown>) {
|
||||||
c: any,
|
|
||||||
workspaceId: string,
|
|
||||||
repoId: string,
|
|
||||||
handoffId: string,
|
|
||||||
createWithInput: Record<string, unknown>
|
|
||||||
) {
|
|
||||||
return await actorClient(c).handoff.getOrCreate(handoffKey(workspaceId, repoId, handoffId), {
|
return await actorClient(c).handoff.getOrCreate(handoffKey(workspaceId, repoId, handoffId), {
|
||||||
createWithInput
|
createWithInput,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,42 +39,30 @@ export async function getOrCreateHistory(c: any, workspaceId: string, repoId: st
|
||||||
return await actorClient(c).history.getOrCreate(historyKey(workspaceId, repoId), {
|
return await actorClient(c).history.getOrCreate(historyKey(workspaceId, repoId), {
|
||||||
createWithInput: {
|
createWithInput: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
repoId
|
repoId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateProjectPrSync(
|
export async function getOrCreateProjectPrSync(c: any, workspaceId: string, repoId: string, repoPath: string, intervalMs: number) {
|
||||||
c: any,
|
|
||||||
workspaceId: string,
|
|
||||||
repoId: string,
|
|
||||||
repoPath: string,
|
|
||||||
intervalMs: number
|
|
||||||
) {
|
|
||||||
return await actorClient(c).projectPrSync.getOrCreate(projectPrSyncKey(workspaceId, repoId), {
|
return await actorClient(c).projectPrSync.getOrCreate(projectPrSyncKey(workspaceId, repoId), {
|
||||||
createWithInput: {
|
createWithInput: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
repoId,
|
repoId,
|
||||||
repoPath,
|
repoPath,
|
||||||
intervalMs
|
intervalMs,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateProjectBranchSync(
|
export async function getOrCreateProjectBranchSync(c: any, workspaceId: string, repoId: string, repoPath: string, intervalMs: number) {
|
||||||
c: any,
|
|
||||||
workspaceId: string,
|
|
||||||
repoId: string,
|
|
||||||
repoPath: string,
|
|
||||||
intervalMs: number
|
|
||||||
) {
|
|
||||||
return await actorClient(c).projectBranchSync.getOrCreate(projectBranchSyncKey(workspaceId, repoId), {
|
return await actorClient(c).projectBranchSync.getOrCreate(projectBranchSyncKey(workspaceId, repoId), {
|
||||||
createWithInput: {
|
createWithInput: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
repoId,
|
repoId,
|
||||||
repoPath,
|
repoPath,
|
||||||
intervalMs
|
intervalMs,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,12 +75,9 @@ export async function getOrCreateSandboxInstance(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
providerId: ProviderId,
|
providerId: ProviderId,
|
||||||
sandboxId: string,
|
sandboxId: string,
|
||||||
createWithInput: Record<string, unknown>
|
createWithInput: Record<string, unknown>,
|
||||||
) {
|
) {
|
||||||
return await actorClient(c).sandboxInstance.getOrCreate(
|
return await actorClient(c).sandboxInstance.getOrCreate(sandboxInstanceKey(workspaceId, providerId, sandboxId), { createWithInput });
|
||||||
sandboxInstanceKey(workspaceId, providerId, sandboxId),
|
|
||||||
{ createWithInput }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateHandoffStatusSync(
|
export async function getOrCreateHandoffStatusSync(
|
||||||
|
|
@ -117,14 +87,11 @@ export async function getOrCreateHandoffStatusSync(
|
||||||
handoffId: string,
|
handoffId: string,
|
||||||
sandboxId: string,
|
sandboxId: string,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
createWithInput: Record<string, unknown>
|
createWithInput: Record<string, unknown>,
|
||||||
) {
|
) {
|
||||||
return await actorClient(c).handoffStatusSync.getOrCreate(
|
return await actorClient(c).handoffStatusSync.getOrCreate(handoffStatusSyncKey(workspaceId, repoId, handoffId, sandboxId, sessionId), {
|
||||||
handoffStatusSyncKey(workspaceId, repoId, handoffId, sandboxId, sessionId),
|
createWithInput,
|
||||||
{
|
});
|
||||||
createWithInput
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selfProjectPrSync(c: any) {
|
export function selfProjectPrSync(c: any) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const CONTROL = {
|
||||||
start: "handoff.status_sync.control.start",
|
start: "handoff.status_sync.control.start",
|
||||||
stop: "handoff.status_sync.control.stop",
|
stop: "handoff.status_sync.control.stop",
|
||||||
setInterval: "handoff.status_sync.control.set_interval",
|
setInterval: "handoff.status_sync.control.set_interval",
|
||||||
force: "handoff.status_sync.control.force"
|
force: "handoff.status_sync.control.force",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
async function pollSessionStatus(c: { state: HandoffStatusSyncState }): Promise<void> {
|
async function pollSessionStatus(c: { state: HandoffStatusSyncState }): Promise<void> {
|
||||||
|
|
@ -43,7 +43,7 @@ async function pollSessionStatus(c: { state: HandoffStatusSyncState }): Promise<
|
||||||
await parent.syncWorkbenchSessionStatus({
|
await parent.syncWorkbenchSessionStatus({
|
||||||
sessionId: c.state.sessionId,
|
sessionId: c.state.sessionId,
|
||||||
status: status.status,
|
status: status.status,
|
||||||
at: Date.now()
|
at: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ export const handoffStatusSync = actor({
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
||||||
noSleep: true
|
noSleep: true,
|
||||||
},
|
},
|
||||||
createState: (_c, input: HandoffStatusSyncInput): HandoffStatusSyncState => ({
|
createState: (_c, input: HandoffStatusSyncInput): HandoffStatusSyncState => ({
|
||||||
workspaceId: input.workspaceId,
|
workspaceId: input.workspaceId,
|
||||||
|
|
@ -66,7 +66,7 @@ export const handoffStatusSync = actor({
|
||||||
sandboxId: input.sandboxId,
|
sandboxId: input.sandboxId,
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
intervalMs: input.intervalMs,
|
intervalMs: input.intervalMs,
|
||||||
running: true
|
running: true,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async start(c): Promise<void> {
|
async start(c): Promise<void> {
|
||||||
|
|
@ -87,7 +87,7 @@ export const handoffStatusSync = actor({
|
||||||
async force(c): Promise<void> {
|
async force(c): Promise<void> {
|
||||||
const self = selfHandoffStatusSync(c);
|
const self = selfHandoffStatusSync(c);
|
||||||
await self.send(CONTROL.force, {}, { wait: true, timeout: 5 * 60_000 });
|
await self.send(CONTROL.force, {}, { wait: true, timeout: 5 * 60_000 });
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
run: workflow(async (ctx) => {
|
run: workflow(async (ctx) => {
|
||||||
await runWorkflowPollingLoop<HandoffStatusSyncState>(ctx, {
|
await runWorkflowPollingLoop<HandoffStatusSyncState>(ctx, {
|
||||||
|
|
@ -99,10 +99,10 @@ export const handoffStatusSync = actor({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("handoff-status-sync", "poll failed", {
|
logActorWarning("handoff-status-sync", "poll failed", {
|
||||||
error: resolveErrorMessage(error),
|
error: resolveErrorMessage(error),
|
||||||
stack: resolveErrorStack(error)
|
stack: resolveErrorStack(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,3 @@ export default defineConfig({
|
||||||
out: "./src/actors/handoff/db/drizzle",
|
out: "./src/actors/handoff/db/drizzle",
|
||||||
schema: "./src/actors/handoff/db/schema.ts",
|
schema: "./src/actors/handoff/db/schema.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,4 +173,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,4 +149,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -219,4 +219,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,50 +3,50 @@
|
||||||
// Do not hand-edit this file.
|
// Do not hand-edit this file.
|
||||||
|
|
||||||
const journal = {
|
const journal = {
|
||||||
"entries": [
|
entries: [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
idx: 0,
|
||||||
"when": 1770924374665,
|
when: 1770924374665,
|
||||||
"tag": "0000_condemned_maria_hill",
|
tag: "0000_condemned_maria_hill",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
idx: 1,
|
||||||
"when": 1770947251055,
|
when: 1770947251055,
|
||||||
"tag": "0001_rapid_eddie_brock",
|
tag: "0001_rapid_eddie_brock",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 2,
|
idx: 2,
|
||||||
"when": 1770948428907,
|
when: 1770948428907,
|
||||||
"tag": "0002_lazy_moira_mactaggert",
|
tag: "0002_lazy_moira_mactaggert",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 3,
|
idx: 3,
|
||||||
"when": 1771027535276,
|
when: 1771027535276,
|
||||||
"tag": "0003_plucky_bran",
|
tag: "0003_plucky_bran",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 4,
|
idx: 4,
|
||||||
"when": 1771097651912,
|
when: 1771097651912,
|
||||||
"tag": "0004_focused_shuri",
|
tag: "0004_focused_shuri",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 5,
|
idx: 5,
|
||||||
"when": 1771370000000,
|
when: 1771370000000,
|
||||||
"tag": "0005_sandbox_actor_id",
|
tag: "0005_sandbox_actor_id",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 6,
|
idx: 6,
|
||||||
"when": 1773020000000,
|
when: 1773020000000,
|
||||||
"tag": "0006_workbench_sessions",
|
tag: "0006_workbench_sessions",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -241,5 +241,5 @@ PRAGMA foreign_keys=on;
|
||||||
\`created_at\` integer NOT NULL,
|
\`created_at\` integer NOT NULL,
|
||||||
\`updated_at\` integer NOT NULL
|
\`updated_at\` integer NOT NULL
|
||||||
);`,
|
);`,
|
||||||
} as const
|
} as const,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import type {
|
||||||
HandoffWorkbenchSetSessionUnreadInput,
|
HandoffWorkbenchSetSessionUnreadInput,
|
||||||
HandoffWorkbenchSendMessageInput,
|
HandoffWorkbenchSendMessageInput,
|
||||||
HandoffWorkbenchUpdateDraftInput,
|
HandoffWorkbenchUpdateDraftInput,
|
||||||
ProviderId
|
ProviderId,
|
||||||
} from "@openhandoff/shared";
|
} from "@openhandoff/shared";
|
||||||
import { expectQueueResponse } from "../../services/queue.js";
|
import { expectQueueResponse } from "../../services/queue.js";
|
||||||
import { selfHandoff } from "../handles.js";
|
import { selfHandoff } from "../handles.js";
|
||||||
|
|
@ -30,13 +30,9 @@ import {
|
||||||
syncWorkbenchSessionStatus,
|
syncWorkbenchSessionStatus,
|
||||||
setWorkbenchSessionUnread,
|
setWorkbenchSessionUnread,
|
||||||
stopWorkbenchSession,
|
stopWorkbenchSession,
|
||||||
updateWorkbenchDraft
|
updateWorkbenchDraft,
|
||||||
} from "./workbench.js";
|
} from "./workbench.js";
|
||||||
import {
|
import { HANDOFF_QUEUE_NAMES, handoffWorkflowQueueName, runHandoffWorkflow } from "./workflow/index.js";
|
||||||
HANDOFF_QUEUE_NAMES,
|
|
||||||
handoffWorkflowQueueName,
|
|
||||||
runHandoffWorkflow
|
|
||||||
} from "./workflow/index.js";
|
|
||||||
|
|
||||||
export interface HandoffInput {
|
export interface HandoffInput {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
@ -114,7 +110,7 @@ export const handoff = actor({
|
||||||
db: handoffDb,
|
db: handoffDb,
|
||||||
queues: Object.fromEntries(HANDOFF_QUEUE_NAMES.map((name) => [name, queue()])),
|
queues: Object.fromEntries(HANDOFF_QUEUE_NAMES.map((name) => [name, queue()])),
|
||||||
options: {
|
options: {
|
||||||
actionTimeout: 5 * 60_000
|
actionTimeout: 5 * 60_000,
|
||||||
},
|
},
|
||||||
createState: (_c, input: HandoffInput) => ({
|
createState: (_c, input: HandoffInput) => ({
|
||||||
workspaceId: input.workspaceId,
|
workspaceId: input.workspaceId,
|
||||||
|
|
@ -155,17 +151,21 @@ export const handoff = actor({
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
const result = await self.send(handoffWorkflowQueueName("handoff.command.attach"), cmd ?? {}, {
|
const result = await self.send(handoffWorkflowQueueName("handoff.command.attach"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 20_000
|
timeout: 20_000,
|
||||||
});
|
});
|
||||||
return expectQueueResponse<{ target: string; sessionId: string | null }>(result);
|
return expectQueueResponse<{ target: string; sessionId: string | null }>(result);
|
||||||
},
|
},
|
||||||
|
|
||||||
async switch(c): Promise<{ switchTarget: string }> {
|
async switch(c): Promise<{ switchTarget: string }> {
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
const result = await self.send(handoffWorkflowQueueName("handoff.command.switch"), {}, {
|
const result = await self.send(
|
||||||
wait: true,
|
handoffWorkflowQueueName("handoff.command.switch"),
|
||||||
timeout: 20_000
|
{},
|
||||||
});
|
{
|
||||||
|
wait: true,
|
||||||
|
timeout: 20_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
return expectQueueResponse<{ switchTarget: string }>(result);
|
return expectQueueResponse<{ switchTarget: string }>(result);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -173,7 +173,7 @@ export const handoff = actor({
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(handoffWorkflowQueueName("handoff.command.push"), cmd ?? {}, {
|
await self.send(handoffWorkflowQueueName("handoff.command.push"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 180_000
|
timeout: 180_000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -181,7 +181,7 @@ export const handoff = actor({
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(handoffWorkflowQueueName("handoff.command.sync"), cmd ?? {}, {
|
await self.send(handoffWorkflowQueueName("handoff.command.sync"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 30_000
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -189,7 +189,7 @@ export const handoff = actor({
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(handoffWorkflowQueueName("handoff.command.merge"), cmd ?? {}, {
|
await self.send(handoffWorkflowQueueName("handoff.command.merge"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 30_000
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -212,7 +212,7 @@ export const handoff = actor({
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(handoffWorkflowQueueName("handoff.command.kill"), cmd ?? {}, {
|
await self.send(handoffWorkflowQueueName("handoff.command.kill"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 60_000
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -225,18 +225,10 @@ export const handoff = actor({
|
||||||
},
|
},
|
||||||
|
|
||||||
async markWorkbenchUnread(c): Promise<void> {
|
async markWorkbenchUnread(c): Promise<void> {
|
||||||
const self = selfHandoff(c);
|
|
||||||
await self.send(handoffWorkflowQueueName("handoff.command.workbench.mark_unread"), {}, {
|
|
||||||
wait: true,
|
|
||||||
timeout: 20_000,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async renameWorkbenchHandoff(c, input: HandoffWorkbenchRenameInput): Promise<void> {
|
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(
|
await self.send(
|
||||||
handoffWorkflowQueueName("handoff.command.workbench.rename_handoff"),
|
handoffWorkflowQueueName("handoff.command.workbench.mark_unread"),
|
||||||
{ value: input.value } satisfies HandoffWorkbenchValueCommand,
|
{},
|
||||||
{
|
{
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 20_000,
|
timeout: 20_000,
|
||||||
|
|
@ -244,16 +236,20 @@ export const handoff = actor({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async renameWorkbenchHandoff(c, input: HandoffWorkbenchRenameInput): Promise<void> {
|
||||||
|
const self = selfHandoff(c);
|
||||||
|
await self.send(handoffWorkflowQueueName("handoff.command.workbench.rename_handoff"), { value: input.value } satisfies HandoffWorkbenchValueCommand, {
|
||||||
|
wait: true,
|
||||||
|
timeout: 20_000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async renameWorkbenchBranch(c, input: HandoffWorkbenchRenameInput): Promise<void> {
|
async renameWorkbenchBranch(c, input: HandoffWorkbenchRenameInput): Promise<void> {
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(
|
await self.send(handoffWorkflowQueueName("handoff.command.workbench.rename_branch"), { value: input.value } satisfies HandoffWorkbenchValueCommand, {
|
||||||
handoffWorkflowQueueName("handoff.command.workbench.rename_branch"),
|
wait: true,
|
||||||
{ value: input.value } satisfies HandoffWorkbenchValueCommand,
|
timeout: 5 * 60_000,
|
||||||
{
|
});
|
||||||
wait: true,
|
|
||||||
timeout: 5 * 60_000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async createWorkbenchSession(c, input?: { model?: string }): Promise<{ tabId: string }> {
|
async createWorkbenchSession(c, input?: { model?: string }): Promise<{ tabId: string }> {
|
||||||
|
|
@ -339,26 +335,18 @@ export const handoff = actor({
|
||||||
|
|
||||||
async stopWorkbenchSession(c, input: HandoffTabCommand): Promise<void> {
|
async stopWorkbenchSession(c, input: HandoffTabCommand): Promise<void> {
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(
|
await self.send(handoffWorkflowQueueName("handoff.command.workbench.stop_session"), { sessionId: input.tabId } satisfies HandoffWorkbenchSessionCommand, {
|
||||||
handoffWorkflowQueueName("handoff.command.workbench.stop_session"),
|
wait: true,
|
||||||
{ sessionId: input.tabId } satisfies HandoffWorkbenchSessionCommand,
|
timeout: 5 * 60_000,
|
||||||
{
|
});
|
||||||
wait: true,
|
|
||||||
timeout: 5 * 60_000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async syncWorkbenchSessionStatus(c, input: HandoffStatusSyncCommand): Promise<void> {
|
async syncWorkbenchSessionStatus(c, input: HandoffStatusSyncCommand): Promise<void> {
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(
|
await self.send(handoffWorkflowQueueName("handoff.command.workbench.sync_session_status"), input, {
|
||||||
handoffWorkflowQueueName("handoff.command.workbench.sync_session_status"),
|
wait: true,
|
||||||
input,
|
timeout: 20_000,
|
||||||
{
|
});
|
||||||
wait: true,
|
|
||||||
timeout: 20_000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async closeWorkbenchSession(c, input: HandoffTabCommand): Promise<void> {
|
async closeWorkbenchSession(c, input: HandoffTabCommand): Promise<void> {
|
||||||
|
|
@ -375,25 +363,25 @@ export const handoff = actor({
|
||||||
|
|
||||||
async publishWorkbenchPr(c): Promise<void> {
|
async publishWorkbenchPr(c): Promise<void> {
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(handoffWorkflowQueueName("handoff.command.workbench.publish_pr"), {}, {
|
await self.send(
|
||||||
wait: true,
|
handoffWorkflowQueueName("handoff.command.workbench.publish_pr"),
|
||||||
timeout: 10 * 60_000,
|
{},
|
||||||
});
|
{
|
||||||
|
wait: true,
|
||||||
|
timeout: 10 * 60_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async revertWorkbenchFile(c, input: { path: string }): Promise<void> {
|
async revertWorkbenchFile(c, input: { path: string }): Promise<void> {
|
||||||
const self = selfHandoff(c);
|
const self = selfHandoff(c);
|
||||||
await self.send(
|
await self.send(handoffWorkflowQueueName("handoff.command.workbench.revert_file"), input, {
|
||||||
handoffWorkflowQueueName("handoff.command.workbench.revert_file"),
|
wait: true,
|
||||||
input,
|
timeout: 5 * 60_000,
|
||||||
{
|
});
|
||||||
wait: true,
|
},
|
||||||
timeout: 5 * 60_000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
run: workflow(runHandoffWorkflow)
|
run: workflow(runHandoffWorkflow),
|
||||||
});
|
});
|
||||||
|
|
||||||
export { HANDOFF_QUEUE_NAMES };
|
export { HANDOFF_QUEUE_NAMES };
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,7 @@
|
||||||
import { basename } from "node:path";
|
import { basename } from "node:path";
|
||||||
import { asc, eq } from "drizzle-orm";
|
import { asc, eq } from "drizzle-orm";
|
||||||
import { getActorRuntimeContext } from "../context.js";
|
import { getActorRuntimeContext } from "../context.js";
|
||||||
import {
|
import { getOrCreateHandoffStatusSync, getOrCreateProject, getOrCreateWorkspace, getSandboxInstance } from "../handles.js";
|
||||||
getOrCreateHandoffStatusSync,
|
|
||||||
getOrCreateProject,
|
|
||||||
getOrCreateWorkspace,
|
|
||||||
getSandboxInstance,
|
|
||||||
} from "../handles.js";
|
|
||||||
import { handoff as handoffTable, handoffRuntime, handoffWorkbenchSessions } from "./db/schema.js";
|
import { handoff as handoffTable, handoffRuntime, handoffWorkbenchSessions } from "./db/schema.js";
|
||||||
import { getCurrentRecord } from "./workflow/common.js";
|
import { getCurrentRecord } from "./workflow/common.js";
|
||||||
|
|
||||||
|
|
@ -90,11 +85,7 @@ export function shouldMarkSessionUnreadForStatus(meta: { thinkingSinceMs?: numbe
|
||||||
|
|
||||||
async function listSessionMetaRows(c: any, options?: { includeClosed?: boolean }): Promise<Array<any>> {
|
async function listSessionMetaRows(c: any, options?: { includeClosed?: boolean }): Promise<Array<any>> {
|
||||||
await ensureWorkbenchSessionTable(c);
|
await ensureWorkbenchSessionTable(c);
|
||||||
const rows = await c.db
|
const rows = await c.db.select().from(handoffWorkbenchSessions).orderBy(asc(handoffWorkbenchSessions.createdAt)).all();
|
||||||
.select()
|
|
||||||
.from(handoffWorkbenchSessions)
|
|
||||||
.orderBy(asc(handoffWorkbenchSessions.createdAt))
|
|
||||||
.all();
|
|
||||||
const mapped = rows.map((row: any) => ({
|
const mapped = rows.map((row: any) => ({
|
||||||
...row,
|
...row,
|
||||||
id: row.sessionId,
|
id: row.sessionId,
|
||||||
|
|
@ -120,11 +111,7 @@ async function nextSessionName(c: any): Promise<string> {
|
||||||
|
|
||||||
async function readSessionMeta(c: any, sessionId: string): Promise<any | null> {
|
async function readSessionMeta(c: any, sessionId: string): Promise<any | null> {
|
||||||
await ensureWorkbenchSessionTable(c);
|
await ensureWorkbenchSessionTable(c);
|
||||||
const row = await c.db
|
const row = await c.db.select().from(handoffWorkbenchSessions).where(eq(handoffWorkbenchSessions.sessionId, sessionId)).get();
|
||||||
.select()
|
|
||||||
.from(handoffWorkbenchSessions)
|
|
||||||
.where(eq(handoffWorkbenchSessions.sessionId, sessionId))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -142,12 +129,15 @@ async function readSessionMeta(c: any, sessionId: string): Promise<any | null> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSessionMeta(c: any, params: {
|
async function ensureSessionMeta(
|
||||||
sessionId: string;
|
c: any,
|
||||||
model?: string;
|
params: {
|
||||||
sessionName?: string;
|
sessionId: string;
|
||||||
unread?: boolean;
|
model?: string;
|
||||||
}): Promise<any> {
|
sessionName?: string;
|
||||||
|
unread?: boolean;
|
||||||
|
},
|
||||||
|
): Promise<any> {
|
||||||
await ensureWorkbenchSessionTable(c);
|
await ensureWorkbenchSessionTable(c);
|
||||||
const existing = await readSessionMeta(c, params.sessionId);
|
const existing = await readSessionMeta(c, params.sessionId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|
@ -202,12 +192,15 @@ function shellFragment(parts: string[]): string {
|
||||||
return parts.join(" && ");
|
return parts.join(" && ");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeInSandbox(c: any, params: {
|
async function executeInSandbox(
|
||||||
sandboxId: string;
|
c: any,
|
||||||
cwd: string;
|
params: {
|
||||||
command: string;
|
sandboxId: string;
|
||||||
label: string;
|
cwd: string;
|
||||||
}): Promise<{ exitCode: number; result: string }> {
|
command: string;
|
||||||
|
label: string;
|
||||||
|
},
|
||||||
|
): Promise<{ exitCode: number; result: string }> {
|
||||||
const { providers } = getActorRuntimeContext();
|
const { providers } = getActorRuntimeContext();
|
||||||
const provider = providers.get(c.state.providerId);
|
const provider = providers.get(c.state.providerId);
|
||||||
return await provider.executeCommand({
|
return await provider.executeCommand({
|
||||||
|
|
@ -226,13 +219,8 @@ function parseGitStatus(output: string): Array<{ path: string; type: "M" | "A" |
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
const status = line.slice(0, 2).trim();
|
const status = line.slice(0, 2).trim();
|
||||||
const rawPath = line.slice(3).trim();
|
const rawPath = line.slice(3).trim();
|
||||||
const path = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() ?? rawPath : rawPath;
|
const path = rawPath.includes(" -> ") ? (rawPath.split(" -> ").pop() ?? rawPath) : rawPath;
|
||||||
const type =
|
const type = status.includes("D") ? "D" : status.includes("A") || status === "??" ? "A" : "M";
|
||||||
status.includes("D")
|
|
||||||
? "D"
|
|
||||||
: status.includes("A") || status === "??"
|
|
||||||
? "A"
|
|
||||||
: "M";
|
|
||||||
return { path, type };
|
return { path, type };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -312,10 +300,7 @@ function buildFileTree(paths: string[]): Array<any> {
|
||||||
|
|
||||||
async function collectWorkbenchGitState(c: any, record: any) {
|
async function collectWorkbenchGitState(c: any, record: any) {
|
||||||
const activeSandboxId = record.activeSandboxId;
|
const activeSandboxId = record.activeSandboxId;
|
||||||
const activeSandbox =
|
const activeSandbox = activeSandboxId != null ? ((record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === activeSandboxId) ?? null) : null;
|
||||||
activeSandboxId != null
|
|
||||||
? (record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === activeSandboxId) ?? null
|
|
||||||
: null;
|
|
||||||
const cwd = activeSandbox?.cwd ?? record.sandboxes?.[0]?.cwd ?? null;
|
const cwd = activeSandbox?.cwd ?? record.sandboxes?.[0]?.cwd ?? null;
|
||||||
if (!activeSandboxId || !cwd) {
|
if (!activeSandboxId || !cwd) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -423,12 +408,7 @@ async function readPullRequestSummary(c: any, branchName: string | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const project = await getOrCreateProject(
|
const project = await getOrCreateProject(c, c.state.workspaceId, c.state.repoId, c.state.repoRemote);
|
||||||
c,
|
|
||||||
c.state.workspaceId,
|
|
||||||
c.state.repoId,
|
|
||||||
c.state.repoRemote,
|
|
||||||
);
|
|
||||||
return await project.getPullRequestForBranch({ branchName });
|
return await project.getPullRequestForBranch({ branchName });
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -528,8 +508,7 @@ export async function renameWorkbenchBranch(c: any, value: string): Promise<void
|
||||||
if (!record.activeSandboxId) {
|
if (!record.activeSandboxId) {
|
||||||
throw new Error("cannot rename branch without an active sandbox");
|
throw new Error("cannot rename branch without an active sandbox");
|
||||||
}
|
}
|
||||||
const activeSandbox =
|
const activeSandbox = (record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === record.activeSandboxId) ?? null;
|
||||||
(record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === record.activeSandboxId) ?? null;
|
|
||||||
if (!activeSandbox?.cwd) {
|
if (!activeSandbox?.cwd) {
|
||||||
throw new Error("cannot rename branch without a sandbox cwd");
|
throw new Error("cannot rename branch without a sandbox cwd");
|
||||||
}
|
}
|
||||||
|
|
@ -572,8 +551,7 @@ export async function createWorkbenchSession(c: any, model?: string): Promise<{
|
||||||
if (!record.activeSandboxId) {
|
if (!record.activeSandboxId) {
|
||||||
throw new Error("cannot create session without an active sandbox");
|
throw new Error("cannot create session without an active sandbox");
|
||||||
}
|
}
|
||||||
const activeSandbox =
|
const activeSandbox = (record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === record.activeSandboxId) ?? null;
|
||||||
(record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === record.activeSandboxId) ?? null;
|
|
||||||
const cwd = activeSandbox?.cwd ?? record.sandboxes?.[0]?.cwd ?? null;
|
const cwd = activeSandbox?.cwd ?? record.sandboxes?.[0]?.cwd ?? null;
|
||||||
if (!cwd) {
|
if (!cwd) {
|
||||||
throw new Error("cannot create session without a sandbox cwd");
|
throw new Error("cannot create session without a sandbox cwd");
|
||||||
|
|
@ -639,10 +617,7 @@ export async function sendWorkbenchMessage(c: any, sessionId: string, text: stri
|
||||||
|
|
||||||
await ensureSessionMeta(c, { sessionId });
|
await ensureSessionMeta(c, { sessionId });
|
||||||
const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, record.activeSandboxId);
|
const sandbox = getSandboxInstance(c, c.state.workspaceId, c.state.providerId, record.activeSandboxId);
|
||||||
const prompt = [
|
const prompt = [text.trim(), ...attachments.map((attachment: any) => `@ ${attachment.filePath}:${attachment.lineNumber}\n${attachment.lineContent}`)]
|
||||||
text.trim(),
|
|
||||||
...attachments.map((attachment: any) => `@ ${attachment.filePath}:${attachment.lineNumber}\n${attachment.lineContent}`),
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
if (!prompt) {
|
if (!prompt) {
|
||||||
|
|
@ -673,23 +648,15 @@ export async function sendWorkbenchMessage(c: any, sessionId: string, text: stri
|
||||||
.where(eq(handoffRuntime.id, 1))
|
.where(eq(handoffRuntime.id, 1))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
const sync = await getOrCreateHandoffStatusSync(
|
const sync = await getOrCreateHandoffStatusSync(c, c.state.workspaceId, c.state.repoId, c.state.handoffId, record.activeSandboxId, sessionId, {
|
||||||
c,
|
workspaceId: c.state.workspaceId,
|
||||||
c.state.workspaceId,
|
repoId: c.state.repoId,
|
||||||
c.state.repoId,
|
handoffId: c.state.handoffId,
|
||||||
c.state.handoffId,
|
providerId: c.state.providerId,
|
||||||
record.activeSandboxId,
|
sandboxId: record.activeSandboxId,
|
||||||
sessionId,
|
sessionId,
|
||||||
{
|
intervalMs: STATUS_SYNC_INTERVAL_MS,
|
||||||
workspaceId: c.state.workspaceId,
|
});
|
||||||
repoId: c.state.repoId,
|
|
||||||
handoffId: c.state.handoffId,
|
|
||||||
providerId: c.state.providerId,
|
|
||||||
sandboxId: record.activeSandboxId,
|
|
||||||
sessionId,
|
|
||||||
intervalMs: STATUS_SYNC_INTERVAL_MS,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await sync.setIntervalMs({ intervalMs: STATUS_SYNC_INTERVAL_MS });
|
await sync.setIntervalMs({ intervalMs: STATUS_SYNC_INTERVAL_MS });
|
||||||
await sync.start();
|
await sync.start();
|
||||||
await sync.force();
|
await sync.force();
|
||||||
|
|
@ -709,12 +676,7 @@ export async function stopWorkbenchSession(c: any, sessionId: string): Promise<v
|
||||||
await notifyWorkbenchUpdated(c);
|
await notifyWorkbenchUpdated(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncWorkbenchSessionStatus(
|
export async function syncWorkbenchSessionStatus(c: any, sessionId: string, status: "running" | "idle" | "error", at: number): Promise<void> {
|
||||||
c: any,
|
|
||||||
sessionId: string,
|
|
||||||
status: "running" | "idle" | "error",
|
|
||||||
at: number,
|
|
||||||
): Promise<void> {
|
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
const record = await ensureWorkbenchSeeded(c);
|
||||||
const meta = await ensureSessionMeta(c, { sessionId });
|
const meta = await ensureSessionMeta(c, { sessionId });
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
@ -821,11 +783,7 @@ export async function publishWorkbenchPr(c: any): Promise<void> {
|
||||||
throw new Error("cannot publish PR without a branch");
|
throw new Error("cannot publish PR without a branch");
|
||||||
}
|
}
|
||||||
const { driver } = getActorRuntimeContext();
|
const { driver } = getActorRuntimeContext();
|
||||||
const created = await driver.github.createPr(
|
const created = await driver.github.createPr(c.state.repoLocalPath, record.branchName, record.title ?? c.state.task);
|
||||||
c.state.repoLocalPath,
|
|
||||||
record.branchName,
|
|
||||||
record.title ?? c.state.task,
|
|
||||||
);
|
|
||||||
await c.db
|
await c.db
|
||||||
.update(handoffTable)
|
.update(handoffTable)
|
||||||
.set({
|
.set({
|
||||||
|
|
@ -842,8 +800,7 @@ export async function revertWorkbenchFile(c: any, path: string): Promise<void> {
|
||||||
if (!record.activeSandboxId) {
|
if (!record.activeSandboxId) {
|
||||||
throw new Error("cannot revert file without an active sandbox");
|
throw new Error("cannot revert file without an active sandbox");
|
||||||
}
|
}
|
||||||
const activeSandbox =
|
const activeSandbox = (record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === record.activeSandboxId) ?? null;
|
||||||
(record.sandboxes ?? []).find((candidate: any) => candidate.sandboxId === record.activeSandboxId) ?? null;
|
|
||||||
if (!activeSandbox?.cwd) {
|
if (!activeSandbox?.cwd) {
|
||||||
throw new Error("cannot revert file without a sandbox cwd");
|
throw new Error("cannot revert file without a sandbox cwd");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: str
|
||||||
promise,
|
promise,
|
||||||
new Promise<T>((_resolve, reject) => {
|
new Promise<T>((_resolve, reject) => {
|
||||||
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
|
|
@ -26,34 +26,27 @@ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: str
|
||||||
export async function handleAttachActivity(loopCtx: any, msg: any): Promise<void> {
|
export async function handleAttachActivity(loopCtx: any, msg: any): Promise<void> {
|
||||||
const record = await getCurrentRecord(loopCtx);
|
const record = await getCurrentRecord(loopCtx);
|
||||||
const { providers } = getActorRuntimeContext();
|
const { providers } = getActorRuntimeContext();
|
||||||
const activeSandbox =
|
const activeSandbox = record.activeSandboxId ? (record.sandboxes.find((sb: any) => sb.sandboxId === record.activeSandboxId) ?? null) : null;
|
||||||
record.activeSandboxId
|
|
||||||
? record.sandboxes.find((sb: any) => sb.sandboxId === record.activeSandboxId) ?? null
|
|
||||||
: null;
|
|
||||||
const provider = providers.get(activeSandbox?.providerId ?? record.providerId);
|
const provider = providers.get(activeSandbox?.providerId ?? record.providerId);
|
||||||
const target = await provider.attachTarget({
|
const target = await provider.attachTarget({
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
sandboxId: record.activeSandboxId ?? ""
|
sandboxId: record.activeSandboxId ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
await appendHistory(loopCtx, "handoff.attach", {
|
await appendHistory(loopCtx, "handoff.attach", {
|
||||||
target: target.target,
|
target: target.target,
|
||||||
sessionId: record.activeSessionId
|
sessionId: record.activeSessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await msg.complete({
|
await msg.complete({
|
||||||
target: target.target,
|
target: target.target,
|
||||||
sessionId: record.activeSessionId
|
sessionId: record.activeSessionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleSwitchActivity(loopCtx: any, msg: any): Promise<void> {
|
export async function handleSwitchActivity(loopCtx: any, msg: any): Promise<void> {
|
||||||
const db = loopCtx.db;
|
const db = loopCtx.db;
|
||||||
const runtime = await db
|
const runtime = await db.select({ switchTarget: handoffRuntime.activeSwitchTarget }).from(handoffRuntime).where(eq(handoffRuntime.id, HANDOFF_ROW_ID)).get();
|
||||||
.select({ switchTarget: handoffRuntime.activeSwitchTarget })
|
|
||||||
.from(handoffRuntime)
|
|
||||||
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
await msg.complete({ switchTarget: runtime?.switchTarget ?? "" });
|
await msg.complete({ switchTarget: runtime?.switchTarget ?? "" });
|
||||||
}
|
}
|
||||||
|
|
@ -61,23 +54,14 @@ export async function handleSwitchActivity(loopCtx: any, msg: any): Promise<void
|
||||||
export async function handlePushActivity(loopCtx: any, msg: any): Promise<void> {
|
export async function handlePushActivity(loopCtx: any, msg: any): Promise<void> {
|
||||||
await pushActiveBranchActivity(loopCtx, {
|
await pushActiveBranchActivity(loopCtx, {
|
||||||
reason: msg.body?.reason ?? null,
|
reason: msg.body?.reason ?? null,
|
||||||
historyKind: "handoff.push"
|
historyKind: "handoff.push",
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleSimpleCommandActivity(
|
export async function handleSimpleCommandActivity(loopCtx: any, msg: any, statusMessage: string, historyKind: string): Promise<void> {
|
||||||
loopCtx: any,
|
|
||||||
msg: any,
|
|
||||||
statusMessage: string,
|
|
||||||
historyKind: string
|
|
||||||
): Promise<void> {
|
|
||||||
const db = loopCtx.db;
|
const db = loopCtx.db;
|
||||||
await db
|
await db.update(handoffRuntime).set({ statusMessage, updatedAt: Date.now() }).where(eq(handoffRuntime.id, HANDOFF_ROW_ID)).run();
|
||||||
.update(handoffRuntime)
|
|
||||||
.set({ statusMessage, updatedAt: Date.now() })
|
|
||||||
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
await appendHistory(loopCtx, historyKind, { reason: msg.body?.reason ?? null });
|
await appendHistory(loopCtx, historyKind, { reason: msg.body?.reason ?? null });
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
|
|
@ -103,8 +87,8 @@ export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<voi
|
||||||
providerId: record.providerId,
|
providerId: record.providerId,
|
||||||
sandboxId: record.activeSandboxId,
|
sandboxId: record.activeSandboxId,
|
||||||
sessionId: record.activeSessionId,
|
sessionId: record.activeSessionId,
|
||||||
intervalMs: 2_000
|
intervalMs: 2_000,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
await withTimeout(sync.stop(), 15_000, "handoff status sync stop");
|
await withTimeout(sync.stop(), 15_000, "handoff status sync stop");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -114,7 +98,7 @@ export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<voi
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
sandboxId: record.activeSandboxId,
|
sandboxId: record.activeSandboxId,
|
||||||
sessionId: record.activeSessionId,
|
sessionId: record.activeSessionId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,8 +106,7 @@ export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<voi
|
||||||
if (record.activeSandboxId) {
|
if (record.activeSandboxId) {
|
||||||
await setHandoffState(loopCtx, "archive_release_sandbox", "releasing sandbox");
|
await setHandoffState(loopCtx, "archive_release_sandbox", "releasing sandbox");
|
||||||
const { providers } = getActorRuntimeContext();
|
const { providers } = getActorRuntimeContext();
|
||||||
const activeSandbox =
|
const activeSandbox = record.sandboxes.find((sb: any) => sb.sandboxId === record.activeSandboxId) ?? null;
|
||||||
record.sandboxes.find((sb: any) => sb.sandboxId === record.activeSandboxId) ?? null;
|
|
||||||
const provider = providers.get(activeSandbox?.providerId ?? record.providerId);
|
const provider = providers.get(activeSandbox?.providerId ?? record.providerId);
|
||||||
const workspaceId = loopCtx.state.workspaceId;
|
const workspaceId = loopCtx.state.workspaceId;
|
||||||
const repoId = loopCtx.state.repoId;
|
const repoId = loopCtx.state.repoId;
|
||||||
|
|
@ -135,28 +118,24 @@ export async function handleArchiveActivity(loopCtx: any, msg: any): Promise<voi
|
||||||
void withTimeout(
|
void withTimeout(
|
||||||
provider.releaseSandbox({
|
provider.releaseSandbox({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
sandboxId
|
sandboxId,
|
||||||
}),
|
}),
|
||||||
45_000,
|
45_000,
|
||||||
"provider releaseSandbox"
|
"provider releaseSandbox",
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
logActorWarning("handoff.commands", "failed to release sandbox during archive", {
|
logActorWarning("handoff.commands", "failed to release sandbox during archive", {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
repoId,
|
repoId,
|
||||||
handoffId,
|
handoffId,
|
||||||
sandboxId,
|
sandboxId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = loopCtx.db;
|
const db = loopCtx.db;
|
||||||
await setHandoffState(loopCtx, "archive_finalize", "finalizing archive");
|
await setHandoffState(loopCtx, "archive_finalize", "finalizing archive");
|
||||||
await db
|
await db.update(handoffTable).set({ status: "archived", updatedAt: Date.now() }).where(eq(handoffTable.id, HANDOFF_ROW_ID)).run();
|
||||||
.update(handoffTable)
|
|
||||||
.set({ status: "archived", updatedAt: Date.now() })
|
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(handoffRuntime)
|
.update(handoffRuntime)
|
||||||
|
|
@ -176,29 +155,20 @@ export async function killDestroySandboxActivity(loopCtx: any): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { providers } = getActorRuntimeContext();
|
const { providers } = getActorRuntimeContext();
|
||||||
const activeSandbox =
|
const activeSandbox = record.sandboxes.find((sb: any) => sb.sandboxId === record.activeSandboxId) ?? null;
|
||||||
record.sandboxes.find((sb: any) => sb.sandboxId === record.activeSandboxId) ?? null;
|
|
||||||
const provider = providers.get(activeSandbox?.providerId ?? record.providerId);
|
const provider = providers.get(activeSandbox?.providerId ?? record.providerId);
|
||||||
await provider.destroySandbox({
|
await provider.destroySandbox({
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
sandboxId: record.activeSandboxId
|
sandboxId: record.activeSandboxId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function killWriteDbActivity(loopCtx: any, msg: any): Promise<void> {
|
export async function killWriteDbActivity(loopCtx: any, msg: any): Promise<void> {
|
||||||
await setHandoffState(loopCtx, "kill_finalize", "finalizing kill");
|
await setHandoffState(loopCtx, "kill_finalize", "finalizing kill");
|
||||||
const db = loopCtx.db;
|
const db = loopCtx.db;
|
||||||
await db
|
await db.update(handoffTable).set({ status: "killed", updatedAt: Date.now() }).where(eq(handoffTable.id, HANDOFF_ROW_ID)).run();
|
||||||
.update(handoffTable)
|
|
||||||
.set({ status: "killed", updatedAt: Date.now() })
|
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
await db
|
await db.update(handoffRuntime).set({ statusMessage: "killed", updatedAt: Date.now() }).where(eq(handoffRuntime.id, HANDOFF_ROW_ID)).run();
|
||||||
.update(handoffRuntime)
|
|
||||||
.set({ statusMessage: "killed", updatedAt: Date.now() })
|
|
||||||
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
await appendHistory(loopCtx, "handoff.kill", { reason: msg.body?.reason ?? null });
|
await appendHistory(loopCtx, "handoff.kill", { reason: msg.body?.reason ?? null });
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,7 @@ export function resolveErrorDetail(error: unknown): string {
|
||||||
return String(error);
|
return String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nonWorkflowWrapper = messages.find(
|
const nonWorkflowWrapper = messages.find((msg) => !/^Step\s+"[^"]+"\s+failed\b/i.test(msg));
|
||||||
(msg) => !/^Step\s+"[^"]+"\s+failed\b/i.test(msg)
|
|
||||||
);
|
|
||||||
return nonWorkflowWrapper ?? messages[0]!;
|
return nonWorkflowWrapper ?? messages[0]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,18 +56,10 @@ export function buildAgentPrompt(task: string): string {
|
||||||
return task.trim();
|
return task.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setHandoffState(
|
export async function setHandoffState(ctx: any, status: HandoffStatus, statusMessage?: string): Promise<void> {
|
||||||
ctx: any,
|
|
||||||
status: HandoffStatus,
|
|
||||||
statusMessage?: string
|
|
||||||
): Promise<void> {
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const db = ctx.db;
|
const db = ctx.db;
|
||||||
await db
|
await db.update(handoffTable).set({ status, updatedAt: now }).where(eq(handoffTable.id, HANDOFF_ROW_ID)).run();
|
||||||
.update(handoffTable)
|
|
||||||
.set({ status, updatedAt: now })
|
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
if (statusMessage != null) {
|
if (statusMessage != null) {
|
||||||
await db
|
await db
|
||||||
|
|
@ -81,14 +71,14 @@ export async function setHandoffState(
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffRuntime.id,
|
target: handoffRuntime.id,
|
||||||
set: {
|
set: {
|
||||||
statusMessage,
|
statusMessage,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +102,7 @@ export async function getCurrentRecord(ctx: any): Promise<HandoffRecord> {
|
||||||
agentType: handoffTable.agentType,
|
agentType: handoffTable.agentType,
|
||||||
prSubmitted: handoffTable.prSubmitted,
|
prSubmitted: handoffTable.prSubmitted,
|
||||||
createdAt: handoffTable.createdAt,
|
createdAt: handoffTable.createdAt,
|
||||||
updatedAt: handoffTable.updatedAt
|
updatedAt: handoffTable.updatedAt,
|
||||||
})
|
})
|
||||||
.from(handoffTable)
|
.from(handoffTable)
|
||||||
.leftJoin(handoffRuntime, eq(handoffTable.id, handoffRuntime.id))
|
.leftJoin(handoffRuntime, eq(handoffTable.id, handoffRuntime.id))
|
||||||
|
|
@ -176,15 +166,14 @@ export async function getCurrentRecord(ctx: any): Promise<HandoffRecord> {
|
||||||
|
|
||||||
export async function appendHistory(ctx: any, kind: string, payload: Record<string, unknown>): Promise<void> {
|
export async function appendHistory(ctx: any, kind: string, payload: Record<string, unknown>): Promise<void> {
|
||||||
const client = ctx.client();
|
const client = ctx.client();
|
||||||
const history = await client.history.getOrCreate(
|
const history = await client.history.getOrCreate(historyKey(ctx.state.workspaceId, ctx.state.repoId), {
|
||||||
historyKey(ctx.state.workspaceId, ctx.state.repoId),
|
createWithInput: { workspaceId: ctx.state.workspaceId, repoId: ctx.state.repoId },
|
||||||
{ createWithInput: { workspaceId: ctx.state.workspaceId, repoId: ctx.state.repoId } }
|
});
|
||||||
);
|
|
||||||
await history.append({
|
await history.append({
|
||||||
kind,
|
kind,
|
||||||
handoffId: ctx.state.handoffId,
|
handoffId: ctx.state.handoffId,
|
||||||
branchName: ctx.state.branchName,
|
branchName: ctx.state.branchName,
|
||||||
payload
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspace = await getOrCreateWorkspace(ctx, ctx.state.workspaceId);
|
const workspace = await getOrCreateWorkspace(ctx, ctx.state.workspaceId);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import {
|
||||||
initFailedActivity,
|
initFailedActivity,
|
||||||
initStartSandboxInstanceActivity,
|
initStartSandboxInstanceActivity,
|
||||||
initStartStatusSyncActivity,
|
initStartStatusSyncActivity,
|
||||||
initWriteDbActivity
|
initWriteDbActivity,
|
||||||
} from "./init.js";
|
} from "./init.js";
|
||||||
import {
|
import {
|
||||||
handleArchiveActivity,
|
handleArchiveActivity,
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
handleSimpleCommandActivity,
|
handleSimpleCommandActivity,
|
||||||
handleSwitchActivity,
|
handleSwitchActivity,
|
||||||
killDestroySandboxActivity,
|
killDestroySandboxActivity,
|
||||||
killWriteDbActivity
|
killWriteDbActivity,
|
||||||
} from "./commands.js";
|
} from "./commands.js";
|
||||||
import { idleNotifyActivity, idleSubmitPrActivity, statusUpdateActivity } from "./status-sync.js";
|
import { idleNotifyActivity, idleSubmitPrActivity, statusUpdateActivity } from "./status-sync.js";
|
||||||
import { HANDOFF_QUEUE_NAMES } from "./queue.js";
|
import { HANDOFF_QUEUE_NAMES } from "./queue.js";
|
||||||
|
|
@ -58,16 +58,13 @@ const commandHandlers: Record<HandoffQueueName, WorkflowHandler> = {
|
||||||
await loopCtx.step("init-bootstrap-db", async () => initBootstrapDbActivity(loopCtx, body));
|
await loopCtx.step("init-bootstrap-db", async () => initBootstrapDbActivity(loopCtx, body));
|
||||||
await loopCtx.removed("init-enqueue-provision", "step");
|
await loopCtx.removed("init-enqueue-provision", "step");
|
||||||
await loopCtx.removed("init-dispatch-provision-v2", "step");
|
await loopCtx.removed("init-dispatch-provision-v2", "step");
|
||||||
const currentRecord = await loopCtx.step(
|
const currentRecord = await loopCtx.step("init-read-current-record", async () => getCurrentRecord(loopCtx));
|
||||||
"init-read-current-record",
|
|
||||||
async () => getCurrentRecord(loopCtx)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await msg.complete(currentRecord);
|
await msg.complete(currentRecord);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("handoff.workflow", "initialize completion failed", {
|
logActorWarning("handoff.workflow", "initialize completion failed", {
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -104,10 +101,7 @@ const commandHandlers: Record<HandoffQueueName, WorkflowHandler> = {
|
||||||
run: async () => initCreateSessionActivity(loopCtx, body, sandbox, sandboxInstanceReady),
|
run: async () => initCreateSessionActivity(loopCtx, body, sandbox, sandboxInstanceReady),
|
||||||
});
|
});
|
||||||
|
|
||||||
await loopCtx.step(
|
await loopCtx.step("init-write-db", async () => initWriteDbActivity(loopCtx, body, sandbox, session, sandboxInstanceReady));
|
||||||
"init-write-db",
|
|
||||||
async () => initWriteDbActivity(loopCtx, body, sandbox, session, sandboxInstanceReady)
|
|
||||||
);
|
|
||||||
await loopCtx.step("init-start-status-sync", async () => initStartStatusSyncActivity(loopCtx, body, sandbox, session));
|
await loopCtx.step("init-start-status-sync", async () => initStartStatusSyncActivity(loopCtx, body, sandbox, session));
|
||||||
await loopCtx.step("init-complete", async () => initCompleteActivity(loopCtx, body, sandbox, session));
|
await loopCtx.step("init-complete", async () => initCompleteActivity(loopCtx, body, sandbox, session));
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
|
|
@ -130,17 +124,11 @@ const commandHandlers: Record<HandoffQueueName, WorkflowHandler> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
"handoff.command.sync": async (loopCtx, msg) => {
|
"handoff.command.sync": async (loopCtx, msg) => {
|
||||||
await loopCtx.step(
|
await loopCtx.step("handle-sync", async () => handleSimpleCommandActivity(loopCtx, msg, "sync requested", "handoff.sync"));
|
||||||
"handle-sync",
|
|
||||||
async () => handleSimpleCommandActivity(loopCtx, msg, "sync requested", "handoff.sync")
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"handoff.command.merge": async (loopCtx, msg) => {
|
"handoff.command.merge": async (loopCtx, msg) => {
|
||||||
await loopCtx.step(
|
await loopCtx.step("handle-merge", async () => handleSimpleCommandActivity(loopCtx, msg, "merge requested", "handoff.merge"));
|
||||||
"handle-merge",
|
|
||||||
async () => handleSimpleCommandActivity(loopCtx, msg, "merge requested", "handoff.merge")
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"handoff.command.archive": async (loopCtx, msg) => {
|
"handoff.command.archive": async (loopCtx, msg) => {
|
||||||
|
|
@ -185,30 +173,22 @@ const commandHandlers: Record<HandoffQueueName, WorkflowHandler> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
"handoff.command.workbench.rename_session": async (loopCtx, msg) => {
|
"handoff.command.workbench.rename_session": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-rename-session", async () =>
|
await loopCtx.step("workbench-rename-session", async () => renameWorkbenchSession(loopCtx, msg.body.sessionId, msg.body.title));
|
||||||
renameWorkbenchSession(loopCtx, msg.body.sessionId, msg.body.title),
|
|
||||||
);
|
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"handoff.command.workbench.set_session_unread": async (loopCtx, msg) => {
|
"handoff.command.workbench.set_session_unread": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-set-session-unread", async () =>
|
await loopCtx.step("workbench-set-session-unread", async () => setWorkbenchSessionUnread(loopCtx, msg.body.sessionId, msg.body.unread));
|
||||||
setWorkbenchSessionUnread(loopCtx, msg.body.sessionId, msg.body.unread),
|
|
||||||
);
|
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"handoff.command.workbench.update_draft": async (loopCtx, msg) => {
|
"handoff.command.workbench.update_draft": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-update-draft", async () =>
|
await loopCtx.step("workbench-update-draft", async () => updateWorkbenchDraft(loopCtx, msg.body.sessionId, msg.body.text, msg.body.attachments));
|
||||||
updateWorkbenchDraft(loopCtx, msg.body.sessionId, msg.body.text, msg.body.attachments),
|
|
||||||
);
|
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
"handoff.command.workbench.change_model": async (loopCtx, msg) => {
|
"handoff.command.workbench.change_model": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-change-model", async () =>
|
await loopCtx.step("workbench-change-model", async () => changeWorkbenchModel(loopCtx, msg.body.sessionId, msg.body.model));
|
||||||
changeWorkbenchModel(loopCtx, msg.body.sessionId, msg.body.model),
|
|
||||||
);
|
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -231,9 +211,7 @@ const commandHandlers: Record<HandoffQueueName, WorkflowHandler> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
"handoff.command.workbench.sync_session_status": async (loopCtx, msg) => {
|
"handoff.command.workbench.sync_session_status": async (loopCtx, msg) => {
|
||||||
await loopCtx.step("workbench-sync-session-status", async () =>
|
await loopCtx.step("workbench-sync-session-status", async () => syncWorkbenchSessionStatus(loopCtx, msg.body.sessionId, msg.body.status, msg.body.at));
|
||||||
syncWorkbenchSessionStatus(loopCtx, msg.body.sessionId, msg.body.status, msg.body.at),
|
|
||||||
);
|
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -274,14 +252,14 @@ const commandHandlers: Record<HandoffQueueName, WorkflowHandler> = {
|
||||||
}
|
}
|
||||||
await loopCtx.step("idle-notify", async () => idleNotifyActivity(loopCtx));
|
await loopCtx.step("idle-notify", async () => idleNotifyActivity(loopCtx));
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function runHandoffWorkflow(ctx: any): Promise<void> {
|
export async function runHandoffWorkflow(ctx: any): Promise<void> {
|
||||||
await ctx.loop("handoff-command-loop", async (loopCtx: any) => {
|
await ctx.loop("handoff-command-loop", async (loopCtx: any) => {
|
||||||
const msg = await loopCtx.queue.next("next-command", {
|
const msg = await loopCtx.queue.next("next-command", {
|
||||||
names: [...HANDOFF_QUEUE_NAMES],
|
names: [...HANDOFF_QUEUE_NAMES],
|
||||||
completable: true
|
completable: true,
|
||||||
});
|
});
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,11 @@ import {
|
||||||
getOrCreateProject,
|
getOrCreateProject,
|
||||||
getOrCreateSandboxInstance,
|
getOrCreateSandboxInstance,
|
||||||
getSandboxInstance,
|
getSandboxInstance,
|
||||||
selfHandoff
|
selfHandoff,
|
||||||
} from "../../handles.js";
|
} from "../../handles.js";
|
||||||
import { logActorWarning, resolveErrorMessage } from "../../logging.js";
|
import { logActorWarning, resolveErrorMessage } from "../../logging.js";
|
||||||
import { handoff as handoffTable, handoffRuntime, handoffSandboxes } from "../db/schema.js";
|
import { handoff as handoffTable, handoffRuntime, handoffSandboxes } from "../db/schema.js";
|
||||||
import {
|
import { HANDOFF_ROW_ID, appendHistory, buildAgentPrompt, collectErrorMessages, resolveErrorDetail, setHandoffState } from "./common.js";
|
||||||
HANDOFF_ROW_ID,
|
|
||||||
appendHistory,
|
|
||||||
buildAgentPrompt,
|
|
||||||
collectErrorMessages,
|
|
||||||
resolveErrorDetail,
|
|
||||||
setHandoffState
|
|
||||||
} from "./common.js";
|
|
||||||
import { handoffWorkflowQueueName } from "./queue.js";
|
import { handoffWorkflowQueueName } from "./queue.js";
|
||||||
|
|
||||||
const DEFAULT_INIT_CREATE_SANDBOX_ACTIVITY_TIMEOUT_MS = 180_000;
|
const DEFAULT_INIT_CREATE_SANDBOX_ACTIVITY_TIMEOUT_MS = 180_000;
|
||||||
|
|
@ -43,15 +36,11 @@ function debugInit(loopCtx: any, message: string, context?: Record<string, unkno
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
repoId: loopCtx.state.repoId,
|
repoId: loopCtx.state.repoId,
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
...(context ?? {})
|
...(context ?? {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withActivityTimeout<T>(
|
async function withActivityTimeout<T>(timeoutMs: number, label: string, run: () => Promise<T>): Promise<T> {
|
||||||
timeoutMs: number,
|
|
||||||
label: string,
|
|
||||||
run: () => Promise<T>
|
|
||||||
): Promise<T> {
|
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
try {
|
try {
|
||||||
return await Promise.race([
|
return await Promise.race([
|
||||||
|
|
@ -60,7 +49,7 @@ async function withActivityTimeout<T>(
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
|
|
@ -88,7 +77,7 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
||||||
status: "init_bootstrap_db",
|
status: "init_bootstrap_db",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffTable.id,
|
target: handoffTable.id,
|
||||||
|
|
@ -99,8 +88,8 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
||||||
providerId,
|
providerId,
|
||||||
status: "init_bootstrap_db",
|
status: "init_bootstrap_db",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -113,7 +102,7 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage: initialStatusMessage,
|
statusMessage: initialStatusMessage,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffRuntime.id,
|
target: handoffRuntime.id,
|
||||||
|
|
@ -123,8 +112,8 @@ export async function initBootstrapDbActivity(loopCtx: any, body: any): Promise<
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage: initialStatusMessage,
|
statusMessage: initialStatusMessage,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -155,7 +144,7 @@ export async function initEnsureNameActivity(loopCtx: any): Promise<void> {
|
||||||
const existing = await loopCtx.db
|
const existing = await loopCtx.db
|
||||||
.select({
|
.select({
|
||||||
branchName: handoffTable.branchName,
|
branchName: handoffTable.branchName,
|
||||||
title: handoffTable.title
|
title: handoffTable.title,
|
||||||
})
|
})
|
||||||
.from(handoffTable)
|
.from(handoffTable)
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
||||||
|
|
@ -175,19 +164,12 @@ export async function initEnsureNameActivity(loopCtx: any): Promise<void> {
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
repoId: loopCtx.state.repoId,
|
repoId: loopCtx.state.repoId,
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const remoteBranches = (await driver.git.listRemoteBranches(loopCtx.state.repoLocalPath)).map(
|
const remoteBranches = (await driver.git.listRemoteBranches(loopCtx.state.repoLocalPath)).map((branch: any) => branch.branchName);
|
||||||
(branch: any) => branch.branchName
|
|
||||||
);
|
|
||||||
|
|
||||||
const project = await getOrCreateProject(
|
const project = await getOrCreateProject(loopCtx, loopCtx.state.workspaceId, loopCtx.state.repoId, loopCtx.state.repoRemote);
|
||||||
loopCtx,
|
|
||||||
loopCtx.state.workspaceId,
|
|
||||||
loopCtx.state.repoId,
|
|
||||||
loopCtx.state.repoRemote
|
|
||||||
);
|
|
||||||
const reservedBranches = await project.listReservedBranches({});
|
const reservedBranches = await project.listReservedBranches({});
|
||||||
|
|
||||||
const resolved = resolveCreateFlowDecision({
|
const resolved = resolveCreateFlowDecision({
|
||||||
|
|
@ -195,7 +177,7 @@ export async function initEnsureNameActivity(loopCtx: any): Promise<void> {
|
||||||
explicitTitle: loopCtx.state.explicitTitle ?? undefined,
|
explicitTitle: loopCtx.state.explicitTitle ?? undefined,
|
||||||
explicitBranchName: loopCtx.state.explicitBranchName ?? undefined,
|
explicitBranchName: loopCtx.state.explicitBranchName ?? undefined,
|
||||||
localBranches: remoteBranches,
|
localBranches: remoteBranches,
|
||||||
handoffBranches: reservedBranches
|
handoffBranches: reservedBranches,
|
||||||
});
|
});
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
@ -204,7 +186,7 @@ export async function initEnsureNameActivity(loopCtx: any): Promise<void> {
|
||||||
.set({
|
.set({
|
||||||
branchName: resolved.branchName,
|
branchName: resolved.branchName,
|
||||||
title: resolved.title,
|
title: resolved.title,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
||||||
.run();
|
.run();
|
||||||
|
|
@ -218,19 +200,19 @@ export async function initEnsureNameActivity(loopCtx: any): Promise<void> {
|
||||||
.update(handoffRuntime)
|
.update(handoffRuntime)
|
||||||
.set({
|
.set({
|
||||||
statusMessage: "provisioning",
|
statusMessage: "provisioning",
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
await project.registerHandoffBranch({
|
await project.registerHandoffBranch({
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
branchName: resolved.branchName
|
branchName: resolved.branchName,
|
||||||
});
|
});
|
||||||
|
|
||||||
await appendHistory(loopCtx, "handoff.named", {
|
await appendHistory(loopCtx, "handoff.named", {
|
||||||
title: resolved.title,
|
title: resolved.title,
|
||||||
branchName: resolved.branchName
|
branchName: resolved.branchName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,7 +234,7 @@ export async function initCreateSandboxActivity(loopCtx: any, body: any): Promis
|
||||||
debugInit(loopCtx, "init_create_sandbox started", {
|
debugInit(loopCtx, "init_create_sandbox started", {
|
||||||
providerId,
|
providerId,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
supportsSessionReuse: provider.capabilities().supportsSessionReuse
|
supportsSessionReuse: provider.capabilities().supportsSessionReuse,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (provider.capabilities().supportsSessionReuse) {
|
if (provider.capabilities().supportsSessionReuse) {
|
||||||
|
|
@ -274,18 +256,16 @@ export async function initCreateSandboxActivity(loopCtx: any, body: any): Promis
|
||||||
if (sandboxId) {
|
if (sandboxId) {
|
||||||
debugInit(loopCtx, "init_create_sandbox attempting resume", { sandboxId });
|
debugInit(loopCtx, "init_create_sandbox attempting resume", { sandboxId });
|
||||||
try {
|
try {
|
||||||
const resumed = await withActivityTimeout(
|
const resumed = await withActivityTimeout(timeoutMs, "resumeSandbox", async () =>
|
||||||
timeoutMs,
|
provider.resumeSandbox({
|
||||||
"resumeSandbox",
|
|
||||||
async () => provider.resumeSandbox({
|
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
sandboxId
|
sandboxId,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
debugInit(loopCtx, "init_create_sandbox resume succeeded", {
|
debugInit(loopCtx, "init_create_sandbox resume succeeded", {
|
||||||
sandboxId: resumed.sandboxId,
|
sandboxId: resumed.sandboxId,
|
||||||
durationMs: Date.now() - startedAt
|
durationMs: Date.now() - startedAt,
|
||||||
});
|
});
|
||||||
return resumed;
|
return resumed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -294,39 +274,37 @@ export async function initCreateSandboxActivity(loopCtx: any, body: any): Promis
|
||||||
repoId: loopCtx.state.repoId,
|
repoId: loopCtx.state.repoId,
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
sandboxId,
|
sandboxId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debugInit(loopCtx, "init_create_sandbox creating fresh sandbox", {
|
debugInit(loopCtx, "init_create_sandbox creating fresh sandbox", {
|
||||||
branchName: loopCtx.state.branchName
|
branchName: loopCtx.state.branchName,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sandbox = await withActivityTimeout(
|
const sandbox = await withActivityTimeout(timeoutMs, "createSandbox", async () =>
|
||||||
timeoutMs,
|
provider.createSandbox({
|
||||||
"createSandbox",
|
|
||||||
async () => provider.createSandbox({
|
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
repoId: loopCtx.state.repoId,
|
repoId: loopCtx.state.repoId,
|
||||||
repoRemote: loopCtx.state.repoRemote,
|
repoRemote: loopCtx.state.repoRemote,
|
||||||
branchName: loopCtx.state.branchName,
|
branchName: loopCtx.state.branchName,
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
debug: (message, context) => debugInit(loopCtx, message, context)
|
debug: (message, context) => debugInit(loopCtx, message, context),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
debugInit(loopCtx, "init_create_sandbox create succeeded", {
|
debugInit(loopCtx, "init_create_sandbox create succeeded", {
|
||||||
sandboxId: sandbox.sandboxId,
|
sandboxId: sandbox.sandboxId,
|
||||||
durationMs: Date.now() - startedAt
|
durationMs: Date.now() - startedAt,
|
||||||
});
|
});
|
||||||
return sandbox;
|
return sandbox;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugInit(loopCtx, "init_create_sandbox failed", {
|
debugInit(loopCtx, "init_create_sandbox failed", {
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -339,67 +317,49 @@ export async function initEnsureAgentActivity(loopCtx: any, body: any, sandbox:
|
||||||
const provider = providers.get(providerId);
|
const provider = providers.get(providerId);
|
||||||
return await provider.ensureSandboxAgent({
|
return await provider.ensureSandboxAgent({
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
sandboxId: sandbox.sandboxId
|
sandboxId: sandbox.sandboxId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initStartSandboxInstanceActivity(
|
export async function initStartSandboxInstanceActivity(loopCtx: any, body: any, sandbox: any, agent: any): Promise<any> {
|
||||||
loopCtx: any,
|
|
||||||
body: any,
|
|
||||||
sandbox: any,
|
|
||||||
agent: any
|
|
||||||
): Promise<any> {
|
|
||||||
await setHandoffState(loopCtx, "init_start_sandbox_instance", "starting sandbox runtime");
|
await setHandoffState(loopCtx, "init_start_sandbox_instance", "starting sandbox runtime");
|
||||||
try {
|
try {
|
||||||
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
||||||
const sandboxInstance = await getOrCreateSandboxInstance(
|
const sandboxInstance = await getOrCreateSandboxInstance(loopCtx, loopCtx.state.workspaceId, providerId, sandbox.sandboxId, {
|
||||||
loopCtx,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
loopCtx.state.workspaceId,
|
|
||||||
providerId,
|
providerId,
|
||||||
sandbox.sandboxId,
|
sandboxId: sandbox.sandboxId,
|
||||||
{
|
});
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
|
||||||
providerId,
|
|
||||||
sandboxId: sandbox.sandboxId
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await sandboxInstance.ensure({
|
await sandboxInstance.ensure({
|
||||||
metadata: sandbox.metadata,
|
metadata: sandbox.metadata,
|
||||||
status: "ready",
|
status: "ready",
|
||||||
agentEndpoint: agent.endpoint,
|
agentEndpoint: agent.endpoint,
|
||||||
agentToken: agent.token
|
agentToken: agent.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
const actorId = typeof (sandboxInstance as any).resolve === "function"
|
const actorId = typeof (sandboxInstance as any).resolve === "function" ? await (sandboxInstance as any).resolve() : null;
|
||||||
? await (sandboxInstance as any).resolve()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true as const,
|
ok: true as const,
|
||||||
actorId: typeof actorId === "string" ? actorId : null
|
actorId: typeof actorId === "string" ? actorId : null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const detail = error instanceof Error ? error.message : String(error);
|
const detail = error instanceof Error ? error.message : String(error);
|
||||||
return {
|
return {
|
||||||
ok: false as const,
|
ok: false as const,
|
||||||
error: `sandbox-instance ensure failed: ${detail}`
|
error: `sandbox-instance ensure failed: ${detail}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initCreateSessionActivity(
|
export async function initCreateSessionActivity(loopCtx: any, body: any, sandbox: any, sandboxInstanceReady: any): Promise<any> {
|
||||||
loopCtx: any,
|
|
||||||
body: any,
|
|
||||||
sandbox: any,
|
|
||||||
sandboxInstanceReady: any
|
|
||||||
): Promise<any> {
|
|
||||||
await setHandoffState(loopCtx, "init_create_session", "creating agent session");
|
await setHandoffState(loopCtx, "init_create_session", "creating agent session");
|
||||||
if (!sandboxInstanceReady.ok) {
|
if (!sandboxInstanceReady.ok) {
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
status: "error",
|
status: "error",
|
||||||
error: sandboxInstanceReady.error ?? "sandbox instance is not ready"
|
error: sandboxInstanceReady.error ?? "sandbox instance is not ready",
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,15 +367,12 @@ export async function initCreateSessionActivity(
|
||||||
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
||||||
const sandboxInstance = getSandboxInstance(loopCtx, loopCtx.state.workspaceId, providerId, sandbox.sandboxId);
|
const sandboxInstance = getSandboxInstance(loopCtx, loopCtx.state.workspaceId, providerId, sandbox.sandboxId);
|
||||||
|
|
||||||
const cwd =
|
const cwd = sandbox.metadata && typeof (sandbox.metadata as any).cwd === "string" ? ((sandbox.metadata as any).cwd as string) : undefined;
|
||||||
sandbox.metadata && typeof (sandbox.metadata as any).cwd === "string"
|
|
||||||
? ((sandbox.metadata as any).cwd as string)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return await sandboxInstance.createSession({
|
return await sandboxInstance.createSession({
|
||||||
prompt: buildAgentPrompt(loopCtx.state.task),
|
prompt: buildAgentPrompt(loopCtx.state.task),
|
||||||
cwd,
|
cwd,
|
||||||
agent: (loopCtx.state.agentType ?? config.default_agent) as any
|
agent: (loopCtx.state.agentType ?? config.default_agent) as any,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -480,7 +437,7 @@ export async function initWriteDbActivity(
|
||||||
body: any,
|
body: any,
|
||||||
sandbox: any,
|
sandbox: any,
|
||||||
session: any,
|
session: any,
|
||||||
sandboxInstanceReady?: { actorId?: string | null }
|
sandboxInstanceReady?: { actorId?: string | null },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await setHandoffState(loopCtx, "init_write_db", "persisting handoff runtime");
|
await setHandoffState(loopCtx, "init_write_db", "persisting handoff runtime");
|
||||||
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
const providerId = body?.providerId ?? loopCtx.state.providerId;
|
||||||
|
|
@ -490,21 +447,10 @@ export async function initWriteDbActivity(
|
||||||
const sessionId = session?.id ?? null;
|
const sessionId = session?.id ?? null;
|
||||||
const sessionHealthy = Boolean(sessionId) && session?.status !== "error";
|
const sessionHealthy = Boolean(sessionId) && session?.status !== "error";
|
||||||
const activeSessionId = sessionHealthy ? sessionId : null;
|
const activeSessionId = sessionHealthy ? sessionId : null;
|
||||||
const statusMessage =
|
const statusMessage = sessionHealthy ? "session created" : session?.status === "error" ? (session.error ?? "session create failed") : "session unavailable";
|
||||||
sessionHealthy
|
|
||||||
? "session created"
|
|
||||||
: session?.status === "error"
|
|
||||||
? (session.error ?? "session create failed")
|
|
||||||
: "session unavailable";
|
|
||||||
|
|
||||||
const activeCwd =
|
const activeCwd = sandbox.metadata && typeof (sandbox.metadata as any).cwd === "string" ? ((sandbox.metadata as any).cwd as string) : null;
|
||||||
sandbox.metadata && typeof (sandbox.metadata as any).cwd === "string"
|
const sandboxActorId = typeof sandboxInstanceReady?.actorId === "string" && sandboxInstanceReady.actorId.length > 0 ? sandboxInstanceReady.actorId : null;
|
||||||
? ((sandbox.metadata as any).cwd as string)
|
|
||||||
: null;
|
|
||||||
const sandboxActorId =
|
|
||||||
typeof sandboxInstanceReady?.actorId === "string" && sandboxInstanceReady.actorId.length > 0
|
|
||||||
? sandboxInstanceReady.actorId
|
|
||||||
: null;
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(handoffTable)
|
.update(handoffTable)
|
||||||
|
|
@ -512,7 +458,7 @@ export async function initWriteDbActivity(
|
||||||
providerId,
|
providerId,
|
||||||
status: sessionHealthy ? "running" : "error",
|
status: sessionHealthy ? "running" : "error",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
||||||
.run();
|
.run();
|
||||||
|
|
@ -527,7 +473,7 @@ export async function initWriteDbActivity(
|
||||||
cwd: activeCwd,
|
cwd: activeCwd,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffSandboxes.sandboxId,
|
target: handoffSandboxes.sandboxId,
|
||||||
|
|
@ -537,8 +483,8 @@ export async function initWriteDbActivity(
|
||||||
switchTarget: sandbox.switchTarget,
|
switchTarget: sandbox.switchTarget,
|
||||||
cwd: activeCwd,
|
cwd: activeCwd,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -551,7 +497,7 @@ export async function initWriteDbActivity(
|
||||||
activeSwitchTarget: sandbox.switchTarget,
|
activeSwitchTarget: sandbox.switchTarget,
|
||||||
activeCwd,
|
activeCwd,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffRuntime.id,
|
target: handoffRuntime.id,
|
||||||
|
|
@ -561,18 +507,13 @@ export async function initWriteDbActivity(
|
||||||
activeSwitchTarget: sandbox.switchTarget,
|
activeSwitchTarget: sandbox.switchTarget,
|
||||||
activeCwd,
|
activeCwd,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initStartStatusSyncActivity(
|
export async function initStartStatusSyncActivity(loopCtx: any, body: any, sandbox: any, session: any): Promise<void> {
|
||||||
loopCtx: any,
|
|
||||||
body: any,
|
|
||||||
sandbox: any,
|
|
||||||
session: any
|
|
||||||
): Promise<void> {
|
|
||||||
const sessionId = session?.id ?? null;
|
const sessionId = session?.id ?? null;
|
||||||
if (!sessionId || session?.status === "error") {
|
if (!sessionId || session?.status === "error") {
|
||||||
return;
|
return;
|
||||||
|
|
@ -594,8 +535,8 @@ export async function initStartStatusSyncActivity(
|
||||||
providerId,
|
providerId,
|
||||||
sandboxId: sandbox.sandboxId,
|
sandboxId: sandbox.sandboxId,
|
||||||
sessionId,
|
sessionId,
|
||||||
intervalMs: 2_000
|
intervalMs: 2_000,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await sync.start();
|
await sync.start();
|
||||||
|
|
@ -614,21 +555,18 @@ export async function initCompleteActivity(loopCtx: any, body: any, sandbox: any
|
||||||
kind: "handoff.initialized",
|
kind: "handoff.initialized",
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
branchName: loopCtx.state.branchName,
|
branchName: loopCtx.state.branchName,
|
||||||
payload: { providerId, sandboxId: sandbox.sandboxId, sessionId }
|
payload: { providerId, sandboxId: sandbox.sandboxId, sessionId },
|
||||||
});
|
});
|
||||||
|
|
||||||
loopCtx.state.initialized = true;
|
loopCtx.state.initialized = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const detail =
|
const detail = session?.status === "error" ? (session.error ?? "session create failed") : "session unavailable";
|
||||||
session?.status === "error"
|
|
||||||
? (session.error ?? "session create failed")
|
|
||||||
: "session unavailable";
|
|
||||||
await setHandoffState(loopCtx, "error", detail);
|
await setHandoffState(loopCtx, "error", detail);
|
||||||
await appendHistory(loopCtx, "handoff.error", {
|
await appendHistory(loopCtx, "handoff.error", {
|
||||||
detail,
|
detail,
|
||||||
messages: [detail]
|
messages: [detail],
|
||||||
});
|
});
|
||||||
loopCtx.state.initialized = false;
|
loopCtx.state.initialized = false;
|
||||||
}
|
}
|
||||||
|
|
@ -652,7 +590,7 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
||||||
status: "error",
|
status: "error",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffTable.id,
|
target: handoffTable.id,
|
||||||
|
|
@ -663,8 +601,8 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
||||||
providerId,
|
providerId,
|
||||||
status: "error",
|
status: "error",
|
||||||
agentType: loopCtx.state.agentType ?? config.default_agent,
|
agentType: loopCtx.state.agentType ?? config.default_agent,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -677,7 +615,7 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage: detail,
|
statusMessage: detail,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffRuntime.id,
|
target: handoffRuntime.id,
|
||||||
|
|
@ -687,13 +625,13 @@ export async function initFailedActivity(loopCtx: any, error: unknown): Promise<
|
||||||
activeSwitchTarget: null,
|
activeSwitchTarget: null,
|
||||||
activeCwd: null,
|
activeCwd: null,
|
||||||
statusMessage: detail,
|
statusMessage: detail,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
await appendHistory(loopCtx, "handoff.error", {
|
await appendHistory(loopCtx, "handoff.error", {
|
||||||
detail,
|
detail,
|
||||||
messages
|
messages,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@ export interface PushActiveBranchOptions {
|
||||||
historyKind?: string;
|
historyKind?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushActiveBranchActivity(
|
export async function pushActiveBranchActivity(loopCtx: any, options: PushActiveBranchOptions = {}): Promise<void> {
|
||||||
loopCtx: any,
|
|
||||||
options: PushActiveBranchOptions = {}
|
|
||||||
): Promise<void> {
|
|
||||||
const record = await getCurrentRecord(loopCtx);
|
const record = await getCurrentRecord(loopCtx);
|
||||||
const activeSandboxId = record.activeSandboxId;
|
const activeSandboxId = record.activeSandboxId;
|
||||||
const branchName = loopCtx.state.branchName ?? record.branchName;
|
const branchName = loopCtx.state.branchName ?? record.branchName;
|
||||||
|
|
@ -24,8 +21,7 @@ export async function pushActiveBranchActivity(
|
||||||
throw new Error("cannot push: handoff branch is not set");
|
throw new Error("cannot push: handoff branch is not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeSandbox =
|
const activeSandbox = record.sandboxes.find((sandbox: any) => sandbox.sandboxId === activeSandboxId) ?? null;
|
||||||
record.sandboxes.find((sandbox: any) => sandbox.sandboxId === activeSandboxId) ?? null;
|
|
||||||
const providerId = activeSandbox?.providerId ?? record.providerId;
|
const providerId = activeSandbox?.providerId ?? record.providerId;
|
||||||
const cwd = activeSandbox?.cwd ?? null;
|
const cwd = activeSandbox?.cwd ?? null;
|
||||||
if (!cwd) {
|
if (!cwd) {
|
||||||
|
|
@ -53,14 +49,14 @@ export async function pushActiveBranchActivity(
|
||||||
`cd ${JSON.stringify(cwd)}`,
|
`cd ${JSON.stringify(cwd)}`,
|
||||||
"git rev-parse --verify HEAD >/dev/null",
|
"git rev-parse --verify HEAD >/dev/null",
|
||||||
"git config credential.helper '!f() { echo username=x-access-token; echo password=${GH_TOKEN:-$GITHUB_TOKEN}; }; f'",
|
"git config credential.helper '!f() { echo username=x-access-token; echo password=${GH_TOKEN:-$GITHUB_TOKEN}; }; f'",
|
||||||
`git push -u origin ${JSON.stringify(branchName)}`
|
`git push -u origin ${JSON.stringify(branchName)}`,
|
||||||
].join("; ");
|
].join("; ");
|
||||||
|
|
||||||
const result = await provider.executeCommand({
|
const result = await provider.executeCommand({
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
sandboxId: activeSandboxId,
|
sandboxId: activeSandboxId,
|
||||||
command: ["bash", "-lc", JSON.stringify(script)].join(" "),
|
command: ["bash", "-lc", JSON.stringify(script)].join(" "),
|
||||||
label: `git push ${branchName}`
|
label: `git push ${branchName}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
|
|
@ -83,6 +79,6 @@ export async function pushActiveBranchActivity(
|
||||||
await appendHistory(loopCtx, options.historyKind ?? "handoff.push", {
|
await appendHistory(loopCtx, options.historyKind ?? "handoff.push", {
|
||||||
reason: options.reason ?? null,
|
reason: options.reason ?? null,
|
||||||
branchName,
|
branchName,
|
||||||
sandboxId: activeSandboxId
|
sandboxId: activeSandboxId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const HANDOFF_QUEUE_NAMES = [
|
||||||
"handoff.command.workbench.close_session",
|
"handoff.command.workbench.close_session",
|
||||||
"handoff.command.workbench.publish_pr",
|
"handoff.command.workbench.publish_pr",
|
||||||
"handoff.command.workbench.revert_file",
|
"handoff.command.workbench.revert_file",
|
||||||
"handoff.status_sync.result"
|
"handoff.status_sync.result",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function handoffWorkflowQueueName(name: string): string {
|
export function handoffWorkflowQueueName(name: string): string {
|
||||||
|
|
|
||||||
|
|
@ -26,21 +26,16 @@ export async function statusUpdateActivity(loopCtx: any, body: any): Promise<boo
|
||||||
const runtime = await db
|
const runtime = await db
|
||||||
.select({
|
.select({
|
||||||
activeSandboxId: handoffRuntime.activeSandboxId,
|
activeSandboxId: handoffRuntime.activeSandboxId,
|
||||||
activeSessionId: handoffRuntime.activeSessionId
|
activeSessionId: handoffRuntime.activeSessionId,
|
||||||
})
|
})
|
||||||
.from(handoffRuntime)
|
.from(handoffRuntime)
|
||||||
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
const isActive =
|
const isActive = runtime?.activeSandboxId === body.sandboxId && runtime?.activeSessionId === body.sessionId;
|
||||||
runtime?.activeSandboxId === body.sandboxId && runtime?.activeSessionId === body.sessionId;
|
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
await db
|
await db.update(handoffTable).set({ status: newStatus, updatedAt: body.at }).where(eq(handoffTable.id, HANDOFF_ROW_ID)).run();
|
||||||
.update(handoffTable)
|
|
||||||
.set({ status: newStatus, updatedAt: body.at })
|
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(handoffRuntime)
|
.update(handoffRuntime)
|
||||||
|
|
@ -58,7 +53,7 @@ export async function statusUpdateActivity(loopCtx: any, body: any): Promise<boo
|
||||||
await appendHistory(loopCtx, "handoff.status", {
|
await appendHistory(loopCtx, "handoff.status", {
|
||||||
status: body.status,
|
status: body.status,
|
||||||
sessionId: body.sessionId,
|
sessionId: body.sessionId,
|
||||||
sandboxId: body.sandboxId
|
sandboxId: body.sandboxId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
|
@ -78,11 +73,7 @@ export async function idleSubmitPrActivity(loopCtx: any): Promise<void> {
|
||||||
const { driver } = getActorRuntimeContext();
|
const { driver } = getActorRuntimeContext();
|
||||||
const db = loopCtx.db;
|
const db = loopCtx.db;
|
||||||
|
|
||||||
const self = await db
|
const self = await db.select({ prSubmitted: handoffTable.prSubmitted }).from(handoffTable).where(eq(handoffTable.id, HANDOFF_ROW_ID)).get();
|
||||||
.select({ prSubmitted: handoffTable.prSubmitted })
|
|
||||||
.from(handoffTable)
|
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (self && self.prSubmitted) return;
|
if (self && self.prSubmitted) return;
|
||||||
|
|
||||||
|
|
@ -93,7 +84,7 @@ export async function idleSubmitPrActivity(loopCtx: any): Promise<void> {
|
||||||
workspaceId: loopCtx.state.workspaceId,
|
workspaceId: loopCtx.state.workspaceId,
|
||||||
repoId: loopCtx.state.repoId,
|
repoId: loopCtx.state.repoId,
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,34 +95,26 @@ export async function idleSubmitPrActivity(loopCtx: any): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await pushActiveBranchActivity(loopCtx, {
|
await pushActiveBranchActivity(loopCtx, {
|
||||||
reason: "auto_submit_idle",
|
reason: "auto_submit_idle",
|
||||||
historyKind: "handoff.push.auto"
|
historyKind: "handoff.push.auto",
|
||||||
});
|
});
|
||||||
|
|
||||||
const pr = await driver.github.createPr(
|
const pr = await driver.github.createPr(loopCtx.state.repoLocalPath, loopCtx.state.branchName, loopCtx.state.title);
|
||||||
loopCtx.state.repoLocalPath,
|
|
||||||
loopCtx.state.branchName,
|
|
||||||
loopCtx.state.title
|
|
||||||
);
|
|
||||||
|
|
||||||
await db
|
await db.update(handoffTable).set({ prSubmitted: 1, updatedAt: Date.now() }).where(eq(handoffTable.id, HANDOFF_ROW_ID)).run();
|
||||||
.update(handoffTable)
|
|
||||||
.set({ prSubmitted: 1, updatedAt: Date.now() })
|
|
||||||
.where(eq(handoffTable.id, HANDOFF_ROW_ID))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
await appendHistory(loopCtx, "handoff.step", {
|
await appendHistory(loopCtx, "handoff.step", {
|
||||||
step: "pr_submit",
|
step: "pr_submit",
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
branchName: loopCtx.state.branchName,
|
branchName: loopCtx.state.branchName,
|
||||||
prUrl: pr.url,
|
prUrl: pr.url,
|
||||||
prNumber: pr.number
|
prNumber: pr.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
await appendHistory(loopCtx, "handoff.pr_created", {
|
await appendHistory(loopCtx, "handoff.pr_created", {
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
branchName: loopCtx.state.branchName,
|
branchName: loopCtx.state.branchName,
|
||||||
prUrl: pr.url,
|
prUrl: pr.url,
|
||||||
prNumber: pr.number
|
prNumber: pr.number,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const detail = resolveErrorDetail(error);
|
const detail = resolveErrorDetail(error);
|
||||||
|
|
@ -139,7 +122,7 @@ export async function idleSubmitPrActivity(loopCtx: any): Promise<void> {
|
||||||
.update(handoffRuntime)
|
.update(handoffRuntime)
|
||||||
.set({
|
.set({
|
||||||
statusMessage: `pr submit failed: ${detail}`,
|
statusMessage: `pr submit failed: ${detail}`,
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
.where(eq(handoffRuntime.id, HANDOFF_ROW_ID))
|
||||||
.run();
|
.run();
|
||||||
|
|
@ -147,7 +130,7 @@ export async function idleSubmitPrActivity(loopCtx: any): Promise<void> {
|
||||||
await appendHistory(loopCtx, "handoff.pr_create_failed", {
|
await appendHistory(loopCtx, "handoff.pr_create_failed", {
|
||||||
handoffId: loopCtx.state.handoffId,
|
handoffId: loopCtx.state.handoffId,
|
||||||
branchName: loopCtx.state.branchName,
|
branchName: loopCtx.state.branchName,
|
||||||
error: detail
|
error: detail,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,3 @@ export default defineConfig({
|
||||||
out: "./src/actors/history/db/drizzle",
|
out: "./src/actors/history/db/drizzle",
|
||||||
schema: "./src/actors/history/db/schema.ts",
|
schema: "./src/actors/history/db/schema.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,4 +67,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,4 @@
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
// Do not hand-edit this file.
|
// Do not hand-edit this file.
|
||||||
|
|
||||||
const journal = {
|
const journal = {
|
||||||
"entries": [
|
entries: [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
idx: 0,
|
||||||
"when": 1770924375133,
|
when: 1770924375133,
|
||||||
"tag": "0000_watery_bushwacker",
|
tag: "0000_watery_bushwacker",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -25,5 +25,5 @@ export default {
|
||||||
\`created_at\` integer NOT NULL
|
\`created_at\` integer NOT NULL
|
||||||
);
|
);
|
||||||
`,
|
`,
|
||||||
} as const
|
} as const,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ async function appendHistoryRow(loopCtx: any, body: AppendHistoryCommand): Promi
|
||||||
branchName: body.branchName ?? null,
|
branchName: body.branchName ?? null,
|
||||||
kind: body.kind,
|
kind: body.kind,
|
||||||
payloadJson: JSON.stringify(body.payload),
|
payloadJson: JSON.stringify(body.payload),
|
||||||
createdAt: now
|
createdAt: now,
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +45,7 @@ async function runHistoryWorkflow(ctx: any): Promise<void> {
|
||||||
await ctx.loop("history-command-loop", async (loopCtx: any) => {
|
await ctx.loop("history-command-loop", async (loopCtx: any) => {
|
||||||
const msg = await loopCtx.queue.next("next-history-command", {
|
const msg = await loopCtx.queue.next("next-history-command", {
|
||||||
names: [...HISTORY_QUEUE_NAMES],
|
names: [...HISTORY_QUEUE_NAMES],
|
||||||
completable: true
|
completable: true,
|
||||||
});
|
});
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
|
|
@ -63,11 +63,11 @@ async function runHistoryWorkflow(ctx: any): Promise<void> {
|
||||||
export const history = actor({
|
export const history = actor({
|
||||||
db: historyDb,
|
db: historyDb,
|
||||||
queues: {
|
queues: {
|
||||||
"history.command.append": queue()
|
"history.command.append": queue(),
|
||||||
},
|
},
|
||||||
createState: (_c, input: HistoryInput) => ({
|
createState: (_c, input: HistoryInput) => ({
|
||||||
workspaceId: input.workspaceId,
|
workspaceId: input.workspaceId,
|
||||||
repoId: input.repoId
|
repoId: input.repoId,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async append(c, command: AppendHistoryCommand): Promise<void> {
|
async append(c, command: AppendHistoryCommand): Promise<void> {
|
||||||
|
|
@ -91,7 +91,7 @@ export const history = actor({
|
||||||
branchName: events.branchName,
|
branchName: events.branchName,
|
||||||
kind: events.kind,
|
kind: events.kind,
|
||||||
payloadJson: events.payloadJson,
|
payloadJson: events.payloadJson,
|
||||||
createdAt: events.createdAt
|
createdAt: events.createdAt,
|
||||||
})
|
})
|
||||||
.from(events);
|
.from(events);
|
||||||
|
|
||||||
|
|
@ -103,9 +103,9 @@ export const history = actor({
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId
|
repoId: c.state.repoId,
|
||||||
}));
|
}));
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
run: workflow(runHistoryWorkflow)
|
run: workflow(runHistoryWorkflow),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,10 @@ export const registry = setup({
|
||||||
history,
|
history,
|
||||||
projectPrSync,
|
projectPrSync,
|
||||||
projectBranchSync,
|
projectBranchSync,
|
||||||
handoffStatusSync
|
handoffStatusSync,
|
||||||
},
|
},
|
||||||
managerPort: resolveManagerPort(),
|
managerPort: resolveManagerPort(),
|
||||||
managerHost: resolveManagerHost()
|
managerHost: resolveManagerHost(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export * from "./context.js";
|
export * from "./context.js";
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,7 @@ export function handoffKey(workspaceId: string, repoId: string, handoffId: strin
|
||||||
return ["ws", workspaceId, "project", repoId, "handoff", handoffId];
|
return ["ws", workspaceId, "project", repoId, "handoff", handoffId];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sandboxInstanceKey(
|
export function sandboxInstanceKey(workspaceId: string, providerId: string, sandboxId: string): ActorKey {
|
||||||
workspaceId: string,
|
|
||||||
providerId: string,
|
|
||||||
sandboxId: string
|
|
||||||
): ActorKey {
|
|
||||||
return ["ws", workspaceId, "provider", providerId, "sandbox", sandboxId];
|
return ["ws", workspaceId, "provider", providerId, "sandbox", sandboxId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,13 +28,7 @@ export function projectBranchSyncKey(workspaceId: string, repoId: string): Actor
|
||||||
return ["ws", workspaceId, "project", repoId, "branch-sync"];
|
return ["ws", workspaceId, "project", repoId, "branch-sync"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handoffStatusSyncKey(
|
export function handoffStatusSyncKey(workspaceId: string, repoId: string, handoffId: string, sandboxId: string, sessionId: string): ActorKey {
|
||||||
workspaceId: string,
|
|
||||||
repoId: string,
|
|
||||||
handoffId: string,
|
|
||||||
sandboxId: string,
|
|
||||||
sessionId: string
|
|
||||||
): ActorKey {
|
|
||||||
// Include sandbox + session so multiple sandboxes/sessions can be tracked per handoff.
|
// Include sandbox + session so multiple sandboxes/sessions can be tracked per handoff.
|
||||||
return ["ws", workspaceId, "project", repoId, "handoff", handoffId, "status-sync", sandboxId, sessionId];
|
return ["ws", workspaceId, "project", repoId, "handoff", handoffId, "status-sync", sandboxId, sessionId];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,11 @@ export function resolveErrorStack(error: unknown): string | undefined {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logActorWarning(
|
export function logActorWarning(scope: string, message: string, context?: Record<string, unknown>): void {
|
||||||
scope: string,
|
|
||||||
message: string,
|
|
||||||
context?: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
const payload = {
|
const payload = {
|
||||||
scope,
|
scope,
|
||||||
message,
|
message,
|
||||||
...(context ?? {})
|
...(context ?? {}),
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn("[openhandoff][actor:warn]", payload);
|
console.warn("[openhandoff][actor:warn]", payload);
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,7 @@ interface PollingActorContext<TState extends PollingControlState> {
|
||||||
state: TState;
|
state: TState;
|
||||||
abortSignal: AbortSignal;
|
abortSignal: AbortSignal;
|
||||||
queue: {
|
queue: {
|
||||||
nextBatch(options: {
|
nextBatch(options: { names: readonly string[]; timeout: number; count: number; completable: true }): Promise<PollingQueueMessage[]>;
|
||||||
names: readonly string[];
|
|
||||||
timeout: number;
|
|
||||||
count: number;
|
|
||||||
completable: true;
|
|
||||||
}): Promise<PollingQueueMessage[]>;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,21 +34,16 @@ interface RunPollingOptions<TState extends PollingControlState> {
|
||||||
|
|
||||||
export async function runPollingControlLoop<TState extends PollingControlState>(
|
export async function runPollingControlLoop<TState extends PollingControlState>(
|
||||||
c: PollingActorContext<TState>,
|
c: PollingActorContext<TState>,
|
||||||
options: RunPollingOptions<TState>
|
options: RunPollingOptions<TState>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
while (!c.abortSignal.aborted) {
|
while (!c.abortSignal.aborted) {
|
||||||
const messages = normalizeMessages(
|
const messages = normalizeMessages(
|
||||||
await c.queue.nextBatch({
|
await c.queue.nextBatch({
|
||||||
names: [
|
names: [options.control.start, options.control.stop, options.control.setInterval, options.control.force],
|
||||||
options.control.start,
|
|
||||||
options.control.stop,
|
|
||||||
options.control.setInterval,
|
|
||||||
options.control.force
|
|
||||||
],
|
|
||||||
timeout: Math.max(500, c.state.intervalMs),
|
timeout: Math.max(500, c.state.intervalMs),
|
||||||
count: 16,
|
count: 16,
|
||||||
completable: true
|
completable: true,
|
||||||
})
|
}),
|
||||||
) as PollingQueueMessage[];
|
) as PollingQueueMessage[];
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
|
|
@ -94,12 +84,7 @@ export async function runPollingControlLoop<TState extends PollingControlState>(
|
||||||
|
|
||||||
interface WorkflowPollingActorContext<TState extends PollingControlState> {
|
interface WorkflowPollingActorContext<TState extends PollingControlState> {
|
||||||
state: TState;
|
state: TState;
|
||||||
loop(config: {
|
loop(config: { name: string; historyEvery: number; historyKeep: number; run(ctx: WorkflowPollingActorContext<TState>): Promise<unknown> }): Promise<void>;
|
||||||
name: string;
|
|
||||||
historyEvery: number;
|
|
||||||
historyKeep: number;
|
|
||||||
run(ctx: WorkflowPollingActorContext<TState>): Promise<unknown>;
|
|
||||||
}): Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkflowPollingQueueMessage extends PollingQueueMessage {}
|
interface WorkflowPollingQueueMessage extends PollingQueueMessage {}
|
||||||
|
|
@ -107,12 +92,15 @@ interface WorkflowPollingQueueMessage extends PollingQueueMessage {}
|
||||||
interface WorkflowPollingLoopContext<TState extends PollingControlState> {
|
interface WorkflowPollingLoopContext<TState extends PollingControlState> {
|
||||||
state: TState;
|
state: TState;
|
||||||
queue: {
|
queue: {
|
||||||
nextBatch(name: string, options: {
|
nextBatch(
|
||||||
names: readonly string[];
|
name: string,
|
||||||
timeout: number;
|
options: {
|
||||||
count: number;
|
names: readonly string[];
|
||||||
completable: true;
|
timeout: number;
|
||||||
}): Promise<WorkflowPollingQueueMessage[]>;
|
count: number;
|
||||||
|
completable: true;
|
||||||
|
},
|
||||||
|
): Promise<WorkflowPollingQueueMessage[]>;
|
||||||
};
|
};
|
||||||
step<T>(
|
step<T>(
|
||||||
nameOrConfig:
|
nameOrConfig:
|
||||||
|
|
@ -138,12 +126,7 @@ export async function runWorkflowPollingLoop<TState extends PollingControlState>
|
||||||
|
|
||||||
const messages = normalizeMessages(
|
const messages = normalizeMessages(
|
||||||
await loopCtx.queue.nextBatch("next-polling-control-batch", {
|
await loopCtx.queue.nextBatch("next-polling-control-batch", {
|
||||||
names: [
|
names: [options.control.start, options.control.stop, options.control.setInterval, options.control.force],
|
||||||
options.control.start,
|
|
||||||
options.control.stop,
|
|
||||||
options.control.setInterval,
|
|
||||||
options.control.force,
|
|
||||||
],
|
|
||||||
timeout: control.running ? control.intervalMs : 60_000,
|
timeout: control.running ? control.intervalMs : 60_000,
|
||||||
count: 16,
|
count: 16,
|
||||||
completable: true,
|
completable: true,
|
||||||
|
|
@ -172,37 +155,35 @@ export async function runWorkflowPollingLoop<TState extends PollingControlState>
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === options.control.stop) {
|
if (msg.name === options.control.stop) {
|
||||||
await loopCtx.step("control-stop", async () => {
|
await loopCtx.step("control-stop", async () => {
|
||||||
loopCtx.state.running = false;
|
loopCtx.state.running = false;
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.name === options.control.setInterval) {
|
|
||||||
await loopCtx.step("control-set-interval", async () => {
|
|
||||||
const intervalMs = Number((msg.body as { intervalMs?: unknown })?.intervalMs);
|
|
||||||
loopCtx.state.intervalMs = Number.isFinite(intervalMs)
|
|
||||||
? Math.max(500, intervalMs)
|
|
||||||
: loopCtx.state.intervalMs;
|
|
||||||
});
|
|
||||||
await msg.complete({ ok: true });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.name === options.control.force) {
|
|
||||||
await loopCtx.step({
|
|
||||||
name: "control-force",
|
|
||||||
timeout: 5 * 60_000,
|
|
||||||
run: async () => {
|
|
||||||
await options.onPoll(loopCtx as unknown as PollingActorContext<TState>);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await msg.complete({ ok: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.name === options.control.setInterval) {
|
||||||
|
await loopCtx.step("control-set-interval", async () => {
|
||||||
|
const intervalMs = Number((msg.body as { intervalMs?: unknown })?.intervalMs);
|
||||||
|
loopCtx.state.intervalMs = Number.isFinite(intervalMs) ? Math.max(500, intervalMs) : loopCtx.state.intervalMs;
|
||||||
|
});
|
||||||
|
await msg.complete({ ok: true });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.name === options.control.force) {
|
||||||
|
await loopCtx.step({
|
||||||
|
name: "control-force",
|
||||||
|
timeout: 5 * 60_000,
|
||||||
|
run: async () => {
|
||||||
|
await options.onPoll(loopCtx as unknown as PollingActorContext<TState>);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await msg.complete({ ok: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,10 @@ const CONTROL = {
|
||||||
start: "project.branch_sync.control.start",
|
start: "project.branch_sync.control.start",
|
||||||
stop: "project.branch_sync.control.stop",
|
stop: "project.branch_sync.control.stop",
|
||||||
setInterval: "project.branch_sync.control.set_interval",
|
setInterval: "project.branch_sync.control.set_interval",
|
||||||
force: "project.branch_sync.control.force"
|
force: "project.branch_sync.control.force",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
async function enrichBranches(
|
async function enrichBranches(workspaceId: string, repoId: string, repoPath: string, git: GitDriver): Promise<EnrichedBranchSnapshot[]> {
|
||||||
workspaceId: string,
|
|
||||||
repoId: string,
|
|
||||||
repoPath: string,
|
|
||||||
git: GitDriver
|
|
||||||
): Promise<EnrichedBranchSnapshot[]> {
|
|
||||||
return await withRepoGitLock(repoPath, async () => {
|
return await withRepoGitLock(repoPath, async () => {
|
||||||
await git.fetch(repoPath);
|
await git.fetch(repoPath);
|
||||||
const branches = await git.listRemoteBranches(repoPath);
|
const branches = await git.listRemoteBranches(repoPath);
|
||||||
|
|
@ -71,7 +66,7 @@ async function enrichBranches(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
repoId,
|
repoId,
|
||||||
branchName: branch.branchName,
|
branchName: branch.branchName,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
branchDiffStat = null;
|
branchDiffStat = null;
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +79,7 @@ async function enrichBranches(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
repoId,
|
repoId,
|
||||||
branchName: branch.branchName,
|
branchName: branch.branchName,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
branchHasUnpushed = false;
|
branchHasUnpushed = false;
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +91,7 @@ async function enrichBranches(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
repoId,
|
repoId,
|
||||||
branchName: branch.branchName,
|
branchName: branch.branchName,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
branchConflicts = false;
|
branchConflicts = false;
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +103,7 @@ async function enrichBranches(
|
||||||
trackedInStack: parentByBranch.has(branch.branchName),
|
trackedInStack: parentByBranch.has(branch.branchName),
|
||||||
diffStat: branchDiffStat,
|
diffStat: branchDiffStat,
|
||||||
hasUnpushed: branchHasUnpushed,
|
hasUnpushed: branchHasUnpushed,
|
||||||
conflictsWithMain: branchConflicts
|
conflictsWithMain: branchConflicts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,14 +127,14 @@ export const projectBranchSync = actor({
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
||||||
noSleep: true
|
noSleep: true,
|
||||||
},
|
},
|
||||||
createState: (_c, input: ProjectBranchSyncInput): ProjectBranchSyncState => ({
|
createState: (_c, input: ProjectBranchSyncInput): ProjectBranchSyncState => ({
|
||||||
workspaceId: input.workspaceId,
|
workspaceId: input.workspaceId,
|
||||||
repoId: input.repoId,
|
repoId: input.repoId,
|
||||||
repoPath: input.repoPath,
|
repoPath: input.repoPath,
|
||||||
intervalMs: input.intervalMs,
|
intervalMs: input.intervalMs,
|
||||||
running: true
|
running: true,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async start(c): Promise<void> {
|
async start(c): Promise<void> {
|
||||||
|
|
@ -160,7 +155,7 @@ export const projectBranchSync = actor({
|
||||||
async force(c): Promise<void> {
|
async force(c): Promise<void> {
|
||||||
const self = selfProjectBranchSync(c);
|
const self = selfProjectBranchSync(c);
|
||||||
await self.send(CONTROL.force, {}, { wait: true, timeout: 5 * 60_000 });
|
await self.send(CONTROL.force, {}, { wait: true, timeout: 5 * 60_000 });
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
run: workflow(async (ctx) => {
|
run: workflow(async (ctx) => {
|
||||||
await runWorkflowPollingLoop<ProjectBranchSyncState>(ctx, {
|
await runWorkflowPollingLoop<ProjectBranchSyncState>(ctx, {
|
||||||
|
|
@ -172,10 +167,10 @@ export const projectBranchSync = actor({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("project-branch-sync", "poll failed", {
|
logActorWarning("project-branch-sync", "poll failed", {
|
||||||
error: resolveErrorMessage(error),
|
error: resolveErrorMessage(error),
|
||||||
stack: resolveErrorStack(error)
|
stack: resolveErrorStack(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ const CONTROL = {
|
||||||
start: "project.pr_sync.control.start",
|
start: "project.pr_sync.control.start",
|
||||||
stop: "project.pr_sync.control.stop",
|
stop: "project.pr_sync.control.stop",
|
||||||
setInterval: "project.pr_sync.control.set_interval",
|
setInterval: "project.pr_sync.control.set_interval",
|
||||||
force: "project.pr_sync.control.force"
|
force: "project.pr_sync.control.force",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
async function pollPrs(c: { state: ProjectPrSyncState }): Promise<void> {
|
async function pollPrs(c: { state: ProjectPrSyncState }): Promise<void> {
|
||||||
|
|
@ -45,14 +45,14 @@ export const projectPrSync = actor({
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
// Polling actors rely on timer-based wakeups; sleeping would pause the timer and stop polling.
|
||||||
noSleep: true
|
noSleep: true,
|
||||||
},
|
},
|
||||||
createState: (_c, input: ProjectPrSyncInput): ProjectPrSyncState => ({
|
createState: (_c, input: ProjectPrSyncInput): ProjectPrSyncState => ({
|
||||||
workspaceId: input.workspaceId,
|
workspaceId: input.workspaceId,
|
||||||
repoId: input.repoId,
|
repoId: input.repoId,
|
||||||
repoPath: input.repoPath,
|
repoPath: input.repoPath,
|
||||||
intervalMs: input.intervalMs,
|
intervalMs: input.intervalMs,
|
||||||
running: true
|
running: true,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async start(c): Promise<void> {
|
async start(c): Promise<void> {
|
||||||
|
|
@ -73,7 +73,7 @@ export const projectPrSync = actor({
|
||||||
async force(c): Promise<void> {
|
async force(c): Promise<void> {
|
||||||
const self = selfProjectPrSync(c);
|
const self = selfProjectPrSync(c);
|
||||||
await self.send(CONTROL.force, {}, { wait: true, timeout: 5 * 60_000 });
|
await self.send(CONTROL.force, {}, { wait: true, timeout: 5 * 60_000 });
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
run: workflow(async (ctx) => {
|
run: workflow(async (ctx) => {
|
||||||
await runWorkflowPollingLoop<ProjectPrSyncState>(ctx, {
|
await runWorkflowPollingLoop<ProjectPrSyncState>(ctx, {
|
||||||
|
|
@ -85,10 +85,10 @@ export const projectPrSync = actor({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("project-pr-sync", "poll failed", {
|
logActorWarning("project-pr-sync", "poll failed", {
|
||||||
error: resolveErrorMessage(error),
|
error: resolveErrorMessage(error),
|
||||||
stack: resolveErrorStack(error)
|
stack: resolveErrorStack(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,9 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { and, desc, eq, isNotNull, ne } from "drizzle-orm";
|
import { and, desc, eq, isNotNull, ne } from "drizzle-orm";
|
||||||
import { Loop } from "rivetkit/workflow";
|
import { Loop } from "rivetkit/workflow";
|
||||||
import type {
|
import type { AgentType, HandoffRecord, HandoffSummary, ProviderId, RepoOverview, RepoStackAction, RepoStackActionResult } from "@openhandoff/shared";
|
||||||
AgentType,
|
|
||||||
HandoffRecord,
|
|
||||||
HandoffSummary,
|
|
||||||
ProviderId,
|
|
||||||
RepoOverview,
|
|
||||||
RepoStackAction,
|
|
||||||
RepoStackActionResult
|
|
||||||
} from "@openhandoff/shared";
|
|
||||||
import { getActorRuntimeContext } from "../context.js";
|
import { getActorRuntimeContext } from "../context.js";
|
||||||
import {
|
import { getHandoff, getOrCreateHandoff, getOrCreateHistory, getOrCreateProjectBranchSync, getOrCreateProjectPrSync, selfProject } from "../handles.js";
|
||||||
getHandoff,
|
|
||||||
getOrCreateHandoff,
|
|
||||||
getOrCreateHistory,
|
|
||||||
getOrCreateProjectBranchSync,
|
|
||||||
getOrCreateProjectPrSync,
|
|
||||||
selfProject
|
|
||||||
} from "../handles.js";
|
|
||||||
import { isActorNotFoundError, logActorWarning, resolveErrorMessage } from "../logging.js";
|
import { isActorNotFoundError, logActorWarning, resolveErrorMessage } from "../logging.js";
|
||||||
import { openhandoffRepoClonePath } from "../../services/openhandoff-paths.js";
|
import { openhandoffRepoClonePath } from "../../services/openhandoff-paths.js";
|
||||||
import { expectQueueResponse } from "../../services/queue.js";
|
import { expectQueueResponse } from "../../services/queue.js";
|
||||||
|
|
@ -163,11 +148,7 @@ async function ensureHandoffIndexHydrated(c: any): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await c.db
|
const existing = await c.db.select({ handoffId: handoffIndex.handoffId }).from(handoffIndex).limit(1).get();
|
||||||
.select({ handoffId: handoffIndex.handoffId })
|
|
||||||
.from(handoffIndex)
|
|
||||||
.limit(1)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
c.state.handoffIndexHydrated = true;
|
c.state.handoffIndexHydrated = true;
|
||||||
|
|
@ -204,7 +185,7 @@ async function ensureHandoffIndexHydrated(c: any): Promise<void> {
|
||||||
handoffId: row.handoffId,
|
handoffId: row.handoffId,
|
||||||
branchName: row.branchName,
|
branchName: row.branchName,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.createdAt
|
updatedAt: row.createdAt,
|
||||||
})
|
})
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.run();
|
.run();
|
||||||
|
|
@ -214,14 +195,14 @@ async function ensureHandoffIndexHydrated(c: any): Promise<void> {
|
||||||
logActorWarning("project", "skipped missing handoffs while hydrating index", {
|
logActorWarning("project", "skipped missing handoffs while hydrating index", {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
skippedMissingHandoffActors
|
skippedMissingHandoffActors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("project", "handoff index hydration from history failed", {
|
logActorWarning("project", "handoff index hydration from history failed", {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,7 +264,7 @@ async function enrichHandoffRecord(c: any, record: HandoffRecord): Promise<Hando
|
||||||
diffStat: branches.diffStat,
|
diffStat: branches.diffStat,
|
||||||
hasUnpushed: branches.hasUnpushed,
|
hasUnpushed: branches.hasUnpushed,
|
||||||
conflictsWithMain: branches.conflictsWithMain,
|
conflictsWithMain: branches.conflictsWithMain,
|
||||||
parentBranch: branches.parentBranch
|
parentBranch: branches.parentBranch,
|
||||||
})
|
})
|
||||||
.from(branches)
|
.from(branches)
|
||||||
.where(eq(branches.branchName, branchName))
|
.where(eq(branches.branchName, branchName))
|
||||||
|
|
@ -298,7 +279,7 @@ async function enrichHandoffRecord(c: any, record: HandoffRecord): Promise<Hando
|
||||||
prAuthor: prCache.prAuthor,
|
prAuthor: prCache.prAuthor,
|
||||||
ciStatus: prCache.ciStatus,
|
ciStatus: prCache.ciStatus,
|
||||||
reviewStatus: prCache.reviewStatus,
|
reviewStatus: prCache.reviewStatus,
|
||||||
reviewer: prCache.reviewer
|
reviewer: prCache.reviewer,
|
||||||
})
|
})
|
||||||
.from(prCache)
|
.from(prCache)
|
||||||
.where(eq(prCache.branchName, branchName))
|
.where(eq(prCache.branchName, branchName))
|
||||||
|
|
@ -315,7 +296,7 @@ async function enrichHandoffRecord(c: any, record: HandoffRecord): Promise<Hando
|
||||||
prAuthor: pr?.prAuthor ?? null,
|
prAuthor: pr?.prAuthor ?? null,
|
||||||
ciStatus: pr?.ciStatus ?? null,
|
ciStatus: pr?.ciStatus ?? null,
|
||||||
reviewStatus: pr?.reviewStatus ?? null,
|
reviewStatus: pr?.reviewStatus ?? null,
|
||||||
reviewer: pr?.reviewer ?? null
|
reviewer: pr?.reviewer ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,14 +309,14 @@ async function ensureProjectMutation(c: any, cmd: EnsureProjectCommand): Promise
|
||||||
.values({
|
.values({
|
||||||
id: 1,
|
id: 1,
|
||||||
remoteUrl: cmd.remoteUrl,
|
remoteUrl: cmd.remoteUrl,
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: repoMeta.id,
|
target: repoMeta.id,
|
||||||
set: {
|
set: {
|
||||||
remoteUrl: cmd.remoteUrl,
|
remoteUrl: cmd.remoteUrl,
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -357,11 +338,7 @@ async function createHandoffMutation(c: any, cmd: CreateHandoffCommand): Promise
|
||||||
if (onBranch) {
|
if (onBranch) {
|
||||||
await forceProjectSync(c, localPath);
|
await forceProjectSync(c, localPath);
|
||||||
|
|
||||||
const branchRow = await c.db
|
const branchRow = await c.db.select({ branchName: branches.branchName }).from(branches).where(eq(branches.branchName, onBranch)).get();
|
||||||
.select({ branchName: branches.branchName })
|
|
||||||
.from(branches)
|
|
||||||
.where(eq(branches.branchName, onBranch))
|
|
||||||
.get();
|
|
||||||
if (!branchRow) {
|
if (!branchRow) {
|
||||||
throw new Error(`Branch not found in repo snapshot: ${onBranch}`);
|
throw new Error(`Branch not found in repo snapshot: ${onBranch}`);
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +346,7 @@ async function createHandoffMutation(c: any, cmd: CreateHandoffCommand): Promise
|
||||||
await registerHandoffBranchMutation(c, {
|
await registerHandoffBranchMutation(c, {
|
||||||
handoffId,
|
handoffId,
|
||||||
branchName: onBranch,
|
branchName: onBranch,
|
||||||
requireExistingRemote: true
|
requireExistingRemote: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -387,11 +364,15 @@ async function createHandoffMutation(c: any, cmd: CreateHandoffCommand): Promise
|
||||||
providerId: cmd.providerId,
|
providerId: cmd.providerId,
|
||||||
agentType: cmd.agentType,
|
agentType: cmd.agentType,
|
||||||
explicitTitle: onBranch ? null : cmd.explicitTitle,
|
explicitTitle: onBranch ? null : cmd.explicitTitle,
|
||||||
explicitBranchName: onBranch ? null : cmd.explicitBranchName
|
explicitBranchName: onBranch ? null : cmd.explicitBranchName,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (onBranch) {
|
if (onBranch) {
|
||||||
await c.db.delete(handoffIndex).where(eq(handoffIndex.handoffId, handoffId)).run().catch(() => {});
|
await c.db
|
||||||
|
.delete(handoffIndex)
|
||||||
|
.where(eq(handoffIndex.handoffId, handoffId))
|
||||||
|
.run()
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -404,7 +385,7 @@ async function createHandoffMutation(c: any, cmd: CreateHandoffCommand): Promise
|
||||||
handoffId,
|
handoffId,
|
||||||
branchName: initialBranchName,
|
branchName: initialBranchName,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.run();
|
.run();
|
||||||
|
|
@ -418,17 +399,14 @@ async function createHandoffMutation(c: any, cmd: CreateHandoffCommand): Promise
|
||||||
handoffId,
|
handoffId,
|
||||||
payload: {
|
payload: {
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
providerId: cmd.providerId
|
providerId: cmd.providerId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function registerHandoffBranchMutation(
|
async function registerHandoffBranchMutation(c: any, cmd: RegisterHandoffBranchCommand): Promise<{ branchName: string; headSha: string }> {
|
||||||
c: any,
|
|
||||||
cmd: RegisterHandoffBranchCommand,
|
|
||||||
): Promise<{ branchName: string; headSha: string }> {
|
|
||||||
const localPath = await ensureProjectReady(c);
|
const localPath = await ensureProjectReady(c);
|
||||||
|
|
||||||
const branchName = cmd.branchName.trim();
|
const branchName = cmd.branchName.trim();
|
||||||
|
|
@ -458,7 +436,7 @@ async function registerHandoffBranchMutation(
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
handoffId: existingOwner.handoffId,
|
handoffId: existingOwner.handoffId,
|
||||||
branchName
|
branchName,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -508,7 +486,7 @@ async function registerHandoffBranchMutation(
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
branchName,
|
branchName,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
stackRows = await driver.stack.listStack(localPath).catch(() => []);
|
stackRows = await driver.stack.listStack(localPath).catch(() => []);
|
||||||
|
|
@ -530,7 +508,7 @@ async function registerHandoffBranchMutation(
|
||||||
trackedInStack: trackedInStack ? 1 : 0,
|
trackedInStack: trackedInStack ? 1 : 0,
|
||||||
firstSeenAt: now,
|
firstSeenAt: now,
|
||||||
lastSeenAt: now,
|
lastSeenAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: branches.branchName,
|
target: branches.branchName,
|
||||||
|
|
@ -539,8 +517,8 @@ async function registerHandoffBranchMutation(
|
||||||
parentBranch,
|
parentBranch,
|
||||||
trackedInStack: trackedInStack ? 1 : 0,
|
trackedInStack: trackedInStack ? 1 : 0,
|
||||||
lastSeenAt: now,
|
lastSeenAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -550,14 +528,14 @@ async function registerHandoffBranchMutation(
|
||||||
handoffId: cmd.handoffId,
|
handoffId: cmd.handoffId,
|
||||||
branchName,
|
branchName,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffIndex.handoffId,
|
target: handoffIndex.handoffId,
|
||||||
set: {
|
set: {
|
||||||
branchName,
|
branchName,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -579,7 +557,7 @@ async function runRepoStackActionMutation(c: any, cmd: RunRepoStackActionCommand
|
||||||
action,
|
action,
|
||||||
executed: false,
|
executed: false,
|
||||||
message: "git-spice is not available for this repo",
|
message: "git-spice is not available for this repo",
|
||||||
at
|
at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -593,11 +571,7 @@ async function runRepoStackActionMutation(c: any, cmd: RunRepoStackActionCommand
|
||||||
await forceProjectSync(c, localPath);
|
await forceProjectSync(c, localPath);
|
||||||
|
|
||||||
if (branchName) {
|
if (branchName) {
|
||||||
const row = await c.db
|
const row = await c.db.select({ branchName: branches.branchName }).from(branches).where(eq(branches.branchName, branchName)).get();
|
||||||
.select({ branchName: branches.branchName })
|
|
||||||
.from(branches)
|
|
||||||
.where(eq(branches.branchName, branchName))
|
|
||||||
.get();
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error(`Branch not found in repo snapshot: ${branchName}`);
|
throw new Error(`Branch not found in repo snapshot: ${branchName}`);
|
||||||
}
|
}
|
||||||
|
|
@ -610,11 +584,7 @@ async function runRepoStackActionMutation(c: any, cmd: RunRepoStackActionCommand
|
||||||
if (parentBranch === branchName) {
|
if (parentBranch === branchName) {
|
||||||
throw new Error("parentBranch must be different from branchName");
|
throw new Error("parentBranch must be different from branchName");
|
||||||
}
|
}
|
||||||
const parentRow = await c.db
|
const parentRow = await c.db.select({ branchName: branches.branchName }).from(branches).where(eq(branches.branchName, parentBranch)).get();
|
||||||
.select({ branchName: branches.branchName })
|
|
||||||
.from(branches)
|
|
||||||
.where(eq(branches.branchName, parentBranch))
|
|
||||||
.get();
|
|
||||||
if (!parentRow) {
|
if (!parentRow) {
|
||||||
throw new Error(`Parent branch not found in repo snapshot: ${parentBranch}`);
|
throw new Error(`Parent branch not found in repo snapshot: ${parentBranch}`);
|
||||||
}
|
}
|
||||||
|
|
@ -646,15 +616,15 @@ async function runRepoStackActionMutation(c: any, cmd: RunRepoStackActionCommand
|
||||||
payload: {
|
payload: {
|
||||||
action,
|
action,
|
||||||
branchName: branchName ?? null,
|
branchName: branchName ?? null,
|
||||||
parentBranch: parentBranch ?? null
|
parentBranch: parentBranch ?? null,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("project", "failed appending repo stack history event", {
|
logActorWarning("project", "failed appending repo stack history event", {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
action,
|
action,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -662,7 +632,7 @@ async function runRepoStackActionMutation(c: any, cmd: RunRepoStackActionCommand
|
||||||
action,
|
action,
|
||||||
executed: true,
|
executed: true,
|
||||||
message: `stack action executed: ${action}`,
|
message: `stack action executed: ${action}`,
|
||||||
at
|
at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -684,7 +654,7 @@ async function applyPrSyncResultMutation(c: any, body: PrSyncResult): Promise<vo
|
||||||
reviewStatus: item.reviewStatus ?? null,
|
reviewStatus: item.reviewStatus ?? null,
|
||||||
reviewer: item.reviewer ?? null,
|
reviewer: item.reviewer ?? null,
|
||||||
fetchedAt: body.at,
|
fetchedAt: body.at,
|
||||||
updatedAt: body.at
|
updatedAt: body.at,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: prCache.branchName,
|
target: prCache.branchName,
|
||||||
|
|
@ -699,8 +669,8 @@ async function applyPrSyncResultMutation(c: any, body: PrSyncResult): Promise<vo
|
||||||
reviewStatus: item.reviewStatus ?? null,
|
reviewStatus: item.reviewStatus ?? null,
|
||||||
reviewer: item.reviewer ?? null,
|
reviewer: item.reviewer ?? null,
|
||||||
fetchedAt: body.at,
|
fetchedAt: body.at,
|
||||||
updatedAt: body.at
|
updatedAt: body.at,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
@ -710,11 +680,7 @@ async function applyPrSyncResultMutation(c: any, body: PrSyncResult): Promise<vo
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = await c.db
|
const row = await c.db.select({ handoffId: handoffIndex.handoffId }).from(handoffIndex).where(eq(handoffIndex.branchName, item.headRefName)).get();
|
||||||
.select({ handoffId: handoffIndex.handoffId })
|
|
||||||
.from(handoffIndex)
|
|
||||||
.where(eq(handoffIndex.branchName, item.headRefName))
|
|
||||||
.get();
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -730,7 +696,7 @@ async function applyPrSyncResultMutation(c: any, body: PrSyncResult): Promise<vo
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
handoffId: row.handoffId,
|
handoffId: row.handoffId,
|
||||||
branchName: item.headRefName,
|
branchName: item.headRefName,
|
||||||
prState: item.state
|
prState: item.state,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -740,7 +706,7 @@ async function applyPrSyncResultMutation(c: any, body: PrSyncResult): Promise<vo
|
||||||
handoffId: row.handoffId,
|
handoffId: row.handoffId,
|
||||||
branchName: item.headRefName,
|
branchName: item.headRefName,
|
||||||
prState: item.state,
|
prState: item.state,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -752,7 +718,7 @@ async function applyBranchSyncResultMutation(c: any, body: BranchSyncResult): Pr
|
||||||
for (const item of body.items) {
|
for (const item of body.items) {
|
||||||
const existing = await c.db
|
const existing = await c.db
|
||||||
.select({
|
.select({
|
||||||
firstSeenAt: branches.firstSeenAt
|
firstSeenAt: branches.firstSeenAt,
|
||||||
})
|
})
|
||||||
.from(branches)
|
.from(branches)
|
||||||
.where(eq(branches.branchName, item.branchName))
|
.where(eq(branches.branchName, item.branchName))
|
||||||
|
|
@ -770,7 +736,7 @@ async function applyBranchSyncResultMutation(c: any, body: BranchSyncResult): Pr
|
||||||
conflictsWithMain: item.conflictsWithMain ? 1 : 0,
|
conflictsWithMain: item.conflictsWithMain ? 1 : 0,
|
||||||
firstSeenAt: existing?.firstSeenAt ?? body.at,
|
firstSeenAt: existing?.firstSeenAt ?? body.at,
|
||||||
lastSeenAt: body.at,
|
lastSeenAt: body.at,
|
||||||
updatedAt: body.at
|
updatedAt: body.at,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: branches.branchName,
|
target: branches.branchName,
|
||||||
|
|
@ -783,16 +749,13 @@ async function applyBranchSyncResultMutation(c: any, body: BranchSyncResult): Pr
|
||||||
conflictsWithMain: item.conflictsWithMain ? 1 : 0,
|
conflictsWithMain: item.conflictsWithMain ? 1 : 0,
|
||||||
firstSeenAt: existing?.firstSeenAt ?? body.at,
|
firstSeenAt: existing?.firstSeenAt ?? body.at,
|
||||||
lastSeenAt: body.at,
|
lastSeenAt: body.at,
|
||||||
updatedAt: body.at
|
updatedAt: body.at,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingRows = await c.db
|
const existingRows = await c.db.select({ branchName: branches.branchName }).from(branches).all();
|
||||||
.select({ branchName: branches.branchName })
|
|
||||||
.from(branches)
|
|
||||||
.all();
|
|
||||||
|
|
||||||
for (const row of existingRows) {
|
for (const row of existingRows) {
|
||||||
if (incoming.has(row.branchName)) {
|
if (incoming.has(row.branchName)) {
|
||||||
|
|
@ -822,62 +785,60 @@ export async function runProjectWorkflow(ctx: any): Promise<void> {
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === "project.command.hydrateHandoffIndex") {
|
if (msg.name === "project.command.hydrateHandoffIndex") {
|
||||||
await loopCtx.step("project-hydrate-handoff-index", async () =>
|
await loopCtx.step("project-hydrate-handoff-index", async () => hydrateHandoffIndexMutation(loopCtx, msg.body as HydrateHandoffIndexCommand));
|
||||||
hydrateHandoffIndexMutation(loopCtx, msg.body as HydrateHandoffIndexCommand),
|
await msg.complete({ ok: true });
|
||||||
);
|
return Loop.continue(undefined);
|
||||||
await msg.complete({ ok: true });
|
}
|
||||||
return Loop.continue(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.name === "project.command.createHandoff") {
|
if (msg.name === "project.command.createHandoff") {
|
||||||
const result = await loopCtx.step({
|
const result = await loopCtx.step({
|
||||||
name: "project-create-handoff",
|
name: "project-create-handoff",
|
||||||
timeout: 12 * 60_000,
|
timeout: 12 * 60_000,
|
||||||
run: async () => createHandoffMutation(loopCtx, msg.body as CreateHandoffCommand),
|
run: async () => createHandoffMutation(loopCtx, msg.body as CreateHandoffCommand),
|
||||||
});
|
});
|
||||||
await msg.complete(result);
|
await msg.complete(result);
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === "project.command.registerHandoffBranch") {
|
if (msg.name === "project.command.registerHandoffBranch") {
|
||||||
const result = await loopCtx.step({
|
const result = await loopCtx.step({
|
||||||
name: "project-register-handoff-branch",
|
name: "project-register-handoff-branch",
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
run: async () => registerHandoffBranchMutation(loopCtx, msg.body as RegisterHandoffBranchCommand),
|
run: async () => registerHandoffBranchMutation(loopCtx, msg.body as RegisterHandoffBranchCommand),
|
||||||
});
|
});
|
||||||
await msg.complete(result);
|
await msg.complete(result);
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === "project.command.runRepoStackAction") {
|
if (msg.name === "project.command.runRepoStackAction") {
|
||||||
const result = await loopCtx.step({
|
const result = await loopCtx.step({
|
||||||
name: "project-run-repo-stack-action",
|
name: "project-run-repo-stack-action",
|
||||||
timeout: 12 * 60_000,
|
timeout: 12 * 60_000,
|
||||||
run: async () => runRepoStackActionMutation(loopCtx, msg.body as RunRepoStackActionCommand),
|
run: async () => runRepoStackActionMutation(loopCtx, msg.body as RunRepoStackActionCommand),
|
||||||
});
|
});
|
||||||
await msg.complete(result);
|
await msg.complete(result);
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === "project.command.applyPrSyncResult") {
|
if (msg.name === "project.command.applyPrSyncResult") {
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "project-apply-pr-sync-result",
|
name: "project-apply-pr-sync-result",
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
run: async () => applyPrSyncResultMutation(loopCtx, msg.body as PrSyncResult),
|
run: async () => applyPrSyncResultMutation(loopCtx, msg.body as PrSyncResult),
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === "project.command.applyBranchSyncResult") {
|
if (msg.name === "project.command.applyBranchSyncResult") {
|
||||||
await loopCtx.step({
|
await loopCtx.step({
|
||||||
name: "project-apply-branch-sync-result",
|
name: "project-apply-branch-sync-result",
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
run: async () => applyBranchSyncResultMutation(loopCtx, msg.body as BranchSyncResult),
|
run: async () => applyBranchSyncResultMutation(loopCtx, msg.body as BranchSyncResult),
|
||||||
});
|
});
|
||||||
await msg.complete({ ok: true });
|
await msg.complete({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
});
|
});
|
||||||
|
|
@ -907,15 +868,9 @@ export const projectActions = {
|
||||||
async listReservedBranches(c: any, _cmd?: ListReservedBranchesCommand): Promise<string[]> {
|
async listReservedBranches(c: any, _cmd?: ListReservedBranchesCommand): Promise<string[]> {
|
||||||
await ensureHandoffIndexHydratedForRead(c);
|
await ensureHandoffIndexHydratedForRead(c);
|
||||||
|
|
||||||
const rows = await c.db
|
const rows = await c.db.select({ branchName: handoffIndex.branchName }).from(handoffIndex).where(isNotNull(handoffIndex.branchName)).all();
|
||||||
.select({ branchName: handoffIndex.branchName })
|
|
||||||
.from(handoffIndex)
|
|
||||||
.where(isNotNull(handoffIndex.branchName))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
return rows
|
return rows.map((row) => row.branchName).filter((name): name is string => typeof name === "string" && name.trim().length > 0);
|
||||||
.map((row) => row.branchName)
|
|
||||||
.filter((name): name is string => typeof name === "string" && name.trim().length > 0);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async registerHandoffBranch(c: any, cmd: RegisterHandoffBranchCommand): Promise<{ branchName: string; headSha: string }> {
|
async registerHandoffBranch(c: any, cmd: RegisterHandoffBranchCommand): Promise<{ branchName: string; headSha: string }> {
|
||||||
|
|
@ -942,11 +897,7 @@ export const projectActions = {
|
||||||
|
|
||||||
await ensureHandoffIndexHydratedForRead(c);
|
await ensureHandoffIndexHydratedForRead(c);
|
||||||
|
|
||||||
const handoffRows = await c.db
|
const handoffRows = await c.db.select({ handoffId: handoffIndex.handoffId }).from(handoffIndex).orderBy(desc(handoffIndex.updatedAt)).all();
|
||||||
.select({ handoffId: handoffIndex.handoffId })
|
|
||||||
.from(handoffIndex)
|
|
||||||
.orderBy(desc(handoffIndex.updatedAt))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
for (const row of handoffRows) {
|
for (const row of handoffRows) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -964,7 +915,7 @@ export const projectActions = {
|
||||||
branchName: record.branchName,
|
branchName: record.branchName,
|
||||||
title: record.title,
|
title: record.title,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
updatedAt: record.updatedAt
|
updatedAt: record.updatedAt,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isStaleHandoffReferenceError(error)) {
|
if (isStaleHandoffReferenceError(error)) {
|
||||||
|
|
@ -972,7 +923,7 @@ export const projectActions = {
|
||||||
logActorWarning("project", "pruned stale handoff index row during summary listing", {
|
logActorWarning("project", "pruned stale handoff index row during summary listing", {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
handoffId: row.handoffId
|
handoffId: row.handoffId,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -980,7 +931,7 @@ export const projectActions = {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
handoffId: row.handoffId,
|
handoffId: row.handoffId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -992,11 +943,7 @@ export const projectActions = {
|
||||||
async getHandoffEnriched(c: any, cmd: GetHandoffEnrichedCommand): Promise<HandoffRecord> {
|
async getHandoffEnriched(c: any, cmd: GetHandoffEnrichedCommand): Promise<HandoffRecord> {
|
||||||
await ensureHandoffIndexHydratedForRead(c);
|
await ensureHandoffIndexHydratedForRead(c);
|
||||||
|
|
||||||
const row = await c.db
|
const row = await c.db.select({ handoffId: handoffIndex.handoffId }).from(handoffIndex).where(eq(handoffIndex.handoffId, cmd.handoffId)).get();
|
||||||
.select({ handoffId: handoffIndex.handoffId })
|
|
||||||
.from(handoffIndex)
|
|
||||||
.where(eq(handoffIndex.handoffId, cmd.handoffId))
|
|
||||||
.get();
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error(`Unknown handoff in repo ${c.state.repoId}: ${cmd.handoffId}`);
|
throw new Error(`Unknown handoff in repo ${c.state.repoId}: ${cmd.handoffId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -1035,7 +982,7 @@ export const projectActions = {
|
||||||
conflictsWithMain: branches.conflictsWithMain,
|
conflictsWithMain: branches.conflictsWithMain,
|
||||||
firstSeenAt: branches.firstSeenAt,
|
firstSeenAt: branches.firstSeenAt,
|
||||||
lastSeenAt: branches.lastSeenAt,
|
lastSeenAt: branches.lastSeenAt,
|
||||||
updatedAt: branches.updatedAt
|
updatedAt: branches.updatedAt,
|
||||||
})
|
})
|
||||||
.from(branches)
|
.from(branches)
|
||||||
.all();
|
.all();
|
||||||
|
|
@ -1044,15 +991,12 @@ export const projectActions = {
|
||||||
.select({
|
.select({
|
||||||
handoffId: handoffIndex.handoffId,
|
handoffId: handoffIndex.handoffId,
|
||||||
branchName: handoffIndex.branchName,
|
branchName: handoffIndex.branchName,
|
||||||
updatedAt: handoffIndex.updatedAt
|
updatedAt: handoffIndex.updatedAt,
|
||||||
})
|
})
|
||||||
.from(handoffIndex)
|
.from(handoffIndex)
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
const handoffMetaByBranch = new Map<
|
const handoffMetaByBranch = new Map<string, { handoffId: string; title: string | null; status: HandoffRecord["status"] | null; updatedAt: number }>();
|
||||||
string,
|
|
||||||
{ handoffId: string; title: string | null; status: HandoffRecord["status"] | null; updatedAt: number }
|
|
||||||
>();
|
|
||||||
|
|
||||||
for (const row of handoffRows) {
|
for (const row of handoffRows) {
|
||||||
if (!row.branchName) {
|
if (!row.branchName) {
|
||||||
|
|
@ -1065,7 +1009,7 @@ export const projectActions = {
|
||||||
handoffId: row.handoffId,
|
handoffId: row.handoffId,
|
||||||
title: record.title ?? null,
|
title: record.title ?? null,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
updatedAt: record.updatedAt
|
updatedAt: record.updatedAt,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isStaleHandoffReferenceError(error)) {
|
if (isStaleHandoffReferenceError(error)) {
|
||||||
|
|
@ -1074,7 +1018,7 @@ export const projectActions = {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
handoffId: row.handoffId,
|
handoffId: row.handoffId,
|
||||||
branchName: row.branchName
|
branchName: row.branchName,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1083,7 +1027,7 @@ export const projectActions = {
|
||||||
repoId: c.state.repoId,
|
repoId: c.state.repoId,
|
||||||
handoffId: row.handoffId,
|
handoffId: row.handoffId,
|
||||||
branchName: row.branchName,
|
branchName: row.branchName,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1096,7 +1040,7 @@ export const projectActions = {
|
||||||
prUrl: prCache.prUrl,
|
prUrl: prCache.prUrl,
|
||||||
ciStatus: prCache.ciStatus,
|
ciStatus: prCache.ciStatus,
|
||||||
reviewStatus: prCache.reviewStatus,
|
reviewStatus: prCache.reviewStatus,
|
||||||
reviewer: prCache.reviewer
|
reviewer: prCache.reviewer,
|
||||||
})
|
})
|
||||||
.from(prCache)
|
.from(prCache)
|
||||||
.all();
|
.all();
|
||||||
|
|
@ -1106,8 +1050,8 @@ export const projectActions = {
|
||||||
branchRowsRaw.map((row) => ({
|
branchRowsRaw.map((row) => ({
|
||||||
branchName: row.branchName,
|
branchName: row.branchName,
|
||||||
parentBranch: row.parentBranch ?? null,
|
parentBranch: row.parentBranch ?? null,
|
||||||
updatedAt: row.updatedAt
|
updatedAt: row.updatedAt,
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const detailByBranch = new Map(branchRowsRaw.map((row) => [row.branchName, row]));
|
const detailByBranch = new Map(branchRowsRaw.map((row) => [row.branchName, row]));
|
||||||
|
|
@ -1135,7 +1079,7 @@ export const projectActions = {
|
||||||
reviewer: pr?.reviewer ?? null,
|
reviewer: pr?.reviewer ?? null,
|
||||||
firstSeenAt: row.firstSeenAt ?? null,
|
firstSeenAt: row.firstSeenAt ?? null,
|
||||||
lastSeenAt: row.lastSeenAt ?? null,
|
lastSeenAt: row.lastSeenAt ?? null,
|
||||||
updatedAt: Math.max(row.updatedAt, handoffMeta?.updatedAt ?? 0)
|
updatedAt: Math.max(row.updatedAt, handoffMeta?.updatedAt ?? 0),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1146,14 +1090,11 @@ export const projectActions = {
|
||||||
baseRef,
|
baseRef,
|
||||||
stackAvailable,
|
stackAvailable,
|
||||||
fetchedAt: now,
|
fetchedAt: now,
|
||||||
branches: branchRows
|
branches: branchRows,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPullRequestForBranch(
|
async getPullRequestForBranch(c: any, cmd: GetPullRequestForBranchCommand): Promise<{ number: number; status: "draft" | "ready" } | null> {
|
||||||
c: any,
|
|
||||||
cmd: GetPullRequestForBranchCommand,
|
|
||||||
): Promise<{ number: number; status: "draft" | "ready" } | null> {
|
|
||||||
const branchName = cmd.branchName?.trim();
|
const branchName = cmd.branchName?.trim();
|
||||||
if (!branchName) {
|
if (!branchName) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1202,5 +1143,5 @@ export const projectActions = {
|
||||||
wait: true,
|
wait: true,
|
||||||
timeout: 5 * 60_000,
|
timeout: 5 * 60_000,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,3 @@ export default defineConfig({
|
||||||
out: "./src/actors/project/db/drizzle",
|
out: "./src/actors/project/db/drizzle",
|
||||||
schema: "./src/actors/project/db/schema.ts",
|
schema: "./src/actors/project/db/schema.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,4 +189,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,4 +213,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -251,4 +251,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,32 @@
|
||||||
// Do not hand-edit this file.
|
// Do not hand-edit this file.
|
||||||
|
|
||||||
const journal = {
|
const journal = {
|
||||||
"entries": [
|
entries: [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
idx: 0,
|
||||||
"when": 1770924376062,
|
when: 1770924376062,
|
||||||
"tag": "0000_stormy_the_hunter",
|
tag: "0000_stormy_the_hunter",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
idx: 1,
|
||||||
"when": 1770947252449,
|
when: 1770947252449,
|
||||||
"tag": "0001_wild_carlie_cooper",
|
tag: "0001_wild_carlie_cooper",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 2,
|
idx: 2,
|
||||||
"when": 1771276338465,
|
when: 1771276338465,
|
||||||
"tag": "0002_far_war_machine",
|
tag: "0002_far_war_machine",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 3,
|
idx: 3,
|
||||||
"when": 1771369000000,
|
when: 1771369000000,
|
||||||
"tag": "0003_busy_legacy",
|
tag: "0003_busy_legacy",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -77,5 +77,5 @@ ALTER TABLE \`branches\` DROP COLUMN \`worktree_path\`;`,
|
||||||
);
|
);
|
||||||
`,
|
`,
|
||||||
m0003: `ALTER TABLE \`branches\` ADD \`tracked_in_stack\` integer;`,
|
m0003: `ALTER TABLE \`branches\` ADD \`tracked_in_stack\` integer;`,
|
||||||
} as const
|
} as const,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,5 @@ export const handoffIndex = sqliteTable("handoff_index", {
|
||||||
handoffId: text("handoff_id").notNull().primaryKey(),
|
handoffId: text("handoff_id").notNull().primaryKey(),
|
||||||
branchName: text("branch_name"),
|
branchName: text("branch_name"),
|
||||||
createdAt: integer("created_at").notNull(),
|
createdAt: integer("created_at").notNull(),
|
||||||
updatedAt: integer("updated_at").notNull()
|
updatedAt: integer("updated_at").notNull(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export const project = actor({
|
||||||
remoteUrl: input.remoteUrl,
|
remoteUrl: input.remoteUrl,
|
||||||
localPath: null as string | null,
|
localPath: null as string | null,
|
||||||
syncActorsStarted: false,
|
syncActorsStarted: false,
|
||||||
handoffIndexHydrated: false
|
handoffIndexHydrated: false,
|
||||||
}),
|
}),
|
||||||
actions: projectActions,
|
actions: projectActions,
|
||||||
run: workflow(runProjectWorkflow),
|
run: workflow(runProjectWorkflow),
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,3 @@ export default defineConfig({
|
||||||
out: "./src/actors/sandbox-instance/db/drizzle",
|
out: "./src/actors/sandbox-instance/db/drizzle",
|
||||||
schema: "./src/actors/sandbox-instance/db/schema.ts",
|
schema: "./src/actors/sandbox-instance/db/schema.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,20 @@
|
||||||
// Do not hand-edit this file.
|
// Do not hand-edit this file.
|
||||||
|
|
||||||
const journal = {
|
const journal = {
|
||||||
"entries": [
|
entries: [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
idx: 0,
|
||||||
"when": 1770924375604,
|
when: 1770924375604,
|
||||||
"tag": "0000_broad_tyrannus",
|
tag: "0000_broad_tyrannus",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
idx: 1,
|
||||||
"when": 1776482400000,
|
when: 1776482400000,
|
||||||
"tag": "0001_sandbox_sessions",
|
tag: "0001_sandbox_sessions",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -57,5 +57,5 @@ CREATE INDEX \`sandbox_session_events_session_id_event_index_idx\` ON \`sandbox_
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE INDEX \`sandbox_session_events_session_id_created_at_idx\` ON \`sandbox_session_events\` (\`session_id\`,\`created_at\`);
|
CREATE INDEX \`sandbox_session_events_session_id_created_at_idx\` ON \`sandbox_session_events\` (\`session_id\`,\`created_at\`);
|
||||||
`,
|
`,
|
||||||
} as const
|
} as const,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,7 @@ const CREATE_SESSION_MAX_ATTEMPTS = 3;
|
||||||
const CREATE_SESSION_RETRY_BASE_MS = 1_000;
|
const CREATE_SESSION_RETRY_BASE_MS = 1_000;
|
||||||
const CREATE_SESSION_STEP_TIMEOUT_MS = 10 * 60_000;
|
const CREATE_SESSION_STEP_TIMEOUT_MS = 10 * 60_000;
|
||||||
|
|
||||||
function normalizeStatusFromEventPayload(
|
function normalizeStatusFromEventPayload(payload: unknown): "running" | "idle" | "error" | null {
|
||||||
payload: unknown,
|
|
||||||
): "running" | "idle" | "error" | null {
|
|
||||||
if (payload && typeof payload === "object") {
|
if (payload && typeof payload === "object") {
|
||||||
const envelope = payload as {
|
const envelope = payload as {
|
||||||
error?: unknown;
|
error?: unknown;
|
||||||
|
|
@ -62,11 +60,7 @@ function normalizeStatusFromEventPayload(
|
||||||
if (lowered.includes("error") || lowered.includes("failed")) {
|
if (lowered.includes("error") || lowered.includes("failed")) {
|
||||||
return "error";
|
return "error";
|
||||||
}
|
}
|
||||||
if (
|
if (lowered.includes("ended") || lowered.includes("complete") || lowered.includes("stopped")) {
|
||||||
lowered.includes("ended") ||
|
|
||||||
lowered.includes("complete") ||
|
|
||||||
lowered.includes("stopped")
|
|
||||||
) {
|
|
||||||
return "idle";
|
return "idle";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -196,12 +190,7 @@ async function derivePersistedSessionStatus(
|
||||||
|
|
||||||
function isTransientSessionCreateError(detail: string): boolean {
|
function isTransientSessionCreateError(detail: string): boolean {
|
||||||
const lowered = detail.toLowerCase();
|
const lowered = detail.toLowerCase();
|
||||||
if (
|
if (lowered.includes("timed out") || lowered.includes("timeout") || lowered.includes("504") || lowered.includes("gateway timeout")) {
|
||||||
lowered.includes("timed out") ||
|
|
||||||
lowered.includes("timeout") ||
|
|
||||||
lowered.includes("504") ||
|
|
||||||
lowered.includes("gateway timeout")
|
|
||||||
) {
|
|
||||||
// ACP timeout errors are expensive and usually deterministic for the same
|
// ACP timeout errors are expensive and usually deterministic for the same
|
||||||
// request; immediate retries spawn additional sessions/processes and make
|
// request; immediate retries spawn additional sessions/processes and make
|
||||||
// recovery harder.
|
// recovery harder.
|
||||||
|
|
@ -209,11 +198,7 @@ function isTransientSessionCreateError(detail: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
lowered.includes("502") ||
|
lowered.includes("502") || lowered.includes("503") || lowered.includes("bad gateway") || lowered.includes("econnreset") || lowered.includes("econnrefused")
|
||||||
lowered.includes("503") ||
|
|
||||||
lowered.includes("bad gateway") ||
|
|
||||||
lowered.includes("econnreset") ||
|
|
||||||
lowered.includes("econnrefused")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,9 +263,7 @@ const SANDBOX_INSTANCE_QUEUE_NAMES = [
|
||||||
|
|
||||||
type SandboxInstanceQueueName = (typeof SANDBOX_INSTANCE_QUEUE_NAMES)[number];
|
type SandboxInstanceQueueName = (typeof SANDBOX_INSTANCE_QUEUE_NAMES)[number];
|
||||||
|
|
||||||
function sandboxInstanceWorkflowQueueName(
|
function sandboxInstanceWorkflowQueueName(name: SandboxInstanceQueueName): SandboxInstanceQueueName {
|
||||||
name: SandboxInstanceQueueName,
|
|
||||||
): SandboxInstanceQueueName {
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,15 +300,15 @@ async function ensureSandboxMutation(c: any, command: EnsureSandboxCommand): Pro
|
||||||
id: SANDBOX_ROW_ID,
|
id: SANDBOX_ROW_ID,
|
||||||
metadataJson,
|
metadataJson,
|
||||||
status: command.status,
|
status: command.status,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: sandboxInstanceTable.id,
|
target: sandboxInstanceTable.id,
|
||||||
set: {
|
set: {
|
||||||
metadataJson,
|
metadataJson,
|
||||||
status: command.status,
|
status: command.status,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
@ -335,17 +318,14 @@ async function updateHealthMutation(c: any, command: HealthSandboxCommand): Prom
|
||||||
.update(sandboxInstanceTable)
|
.update(sandboxInstanceTable)
|
||||||
.set({
|
.set({
|
||||||
status: `${command.status}:${command.message}`,
|
status: `${command.status}:${command.message}`,
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.where(eq(sandboxInstanceTable.id, SANDBOX_ROW_ID))
|
.where(eq(sandboxInstanceTable.id, SANDBOX_ROW_ID))
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function destroySandboxMutation(c: any): Promise<void> {
|
async function destroySandboxMutation(c: any): Promise<void> {
|
||||||
await c.db
|
await c.db.delete(sandboxInstanceTable).where(eq(sandboxInstanceTable.id, SANDBOX_ROW_ID)).run();
|
||||||
.delete(sandboxInstanceTable)
|
|
||||||
.where(eq(sandboxInstanceTable.id, SANDBOX_ROW_ID))
|
|
||||||
.run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createSessionMutation(c: any, command: CreateSessionCommand): Promise<CreateSessionResult> {
|
async function createSessionMutation(c: any, command: CreateSessionCommand): Promise<CreateSessionResult> {
|
||||||
|
|
@ -382,7 +362,7 @@ async function createSessionMutation(c: any, command: CreateSessionCommand): Pro
|
||||||
attempt,
|
attempt,
|
||||||
maxAttempts: CREATE_SESSION_MAX_ATTEMPTS,
|
maxAttempts: CREATE_SESSION_MAX_ATTEMPTS,
|
||||||
waitMs,
|
waitMs,
|
||||||
error: detail
|
error: detail,
|
||||||
});
|
});
|
||||||
await delay(waitMs);
|
await delay(waitMs);
|
||||||
}
|
}
|
||||||
|
|
@ -392,7 +372,7 @@ async function createSessionMutation(c: any, command: CreateSessionCommand): Pro
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
status: "error",
|
status: "error",
|
||||||
error: `sandbox-agent createSession failed after ${attemptsMade} ${attemptLabel}: ${lastDetail}`
|
error: `sandbox-agent createSession failed after ${attemptsMade} ${attemptLabel}: ${lastDetail}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,62 +405,50 @@ async function runSandboxInstanceWorkflow(ctx: any): Promise<void> {
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === "sandboxInstance.command.ensure") {
|
if (msg.name === "sandboxInstance.command.ensure") {
|
||||||
await loopCtx.step("sandbox-instance-ensure", async () =>
|
await loopCtx.step("sandbox-instance-ensure", async () => ensureSandboxMutation(loopCtx, msg.body as EnsureSandboxCommand));
|
||||||
ensureSandboxMutation(loopCtx, msg.body as EnsureSandboxCommand),
|
await msg.complete({ ok: true });
|
||||||
);
|
return Loop.continue(undefined);
|
||||||
await msg.complete({ ok: true });
|
}
|
||||||
return Loop.continue(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.name === "sandboxInstance.command.updateHealth") {
|
if (msg.name === "sandboxInstance.command.updateHealth") {
|
||||||
await loopCtx.step("sandbox-instance-update-health", async () =>
|
await loopCtx.step("sandbox-instance-update-health", async () => updateHealthMutation(loopCtx, msg.body as HealthSandboxCommand));
|
||||||
updateHealthMutation(loopCtx, msg.body as HealthSandboxCommand),
|
await msg.complete({ ok: true });
|
||||||
);
|
return Loop.continue(undefined);
|
||||||
await msg.complete({ ok: true });
|
}
|
||||||
return Loop.continue(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.name === "sandboxInstance.command.destroy") {
|
if (msg.name === "sandboxInstance.command.destroy") {
|
||||||
await loopCtx.step("sandbox-instance-destroy", async () =>
|
await loopCtx.step("sandbox-instance-destroy", async () => destroySandboxMutation(loopCtx));
|
||||||
destroySandboxMutation(loopCtx),
|
await msg.complete({ ok: true });
|
||||||
);
|
return Loop.continue(undefined);
|
||||||
await msg.complete({ ok: true });
|
}
|
||||||
return Loop.continue(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.name === "sandboxInstance.command.createSession") {
|
if (msg.name === "sandboxInstance.command.createSession") {
|
||||||
const result = await loopCtx.step({
|
const result = await loopCtx.step({
|
||||||
name: "sandbox-instance-create-session",
|
name: "sandbox-instance-create-session",
|
||||||
timeout: CREATE_SESSION_STEP_TIMEOUT_MS,
|
timeout: CREATE_SESSION_STEP_TIMEOUT_MS,
|
||||||
run: async () => createSessionMutation(loopCtx, msg.body as CreateSessionCommand),
|
run: async () => createSessionMutation(loopCtx, msg.body as CreateSessionCommand),
|
||||||
});
|
});
|
||||||
await msg.complete(result);
|
await msg.complete(result);
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.name === "sandboxInstance.command.sendPrompt") {
|
if (msg.name === "sandboxInstance.command.sendPrompt") {
|
||||||
await loopCtx.step("sandbox-instance-send-prompt", async () =>
|
await loopCtx.step("sandbox-instance-send-prompt", async () => sendPromptMutation(loopCtx, msg.body as SendPromptCommand));
|
||||||
sendPromptMutation(loopCtx, msg.body as SendPromptCommand),
|
await msg.complete({ ok: true });
|
||||||
);
|
return Loop.continue(undefined);
|
||||||
await msg.complete({ ok: true });
|
}
|
||||||
return Loop.continue(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.name === "sandboxInstance.command.cancelSession") {
|
if (msg.name === "sandboxInstance.command.cancelSession") {
|
||||||
await loopCtx.step("sandbox-instance-cancel-session", async () =>
|
await loopCtx.step("sandbox-instance-cancel-session", async () => cancelSessionMutation(loopCtx, msg.body as SessionControlCommand));
|
||||||
cancelSessionMutation(loopCtx, msg.body as SessionControlCommand),
|
await msg.complete({ ok: true });
|
||||||
);
|
return Loop.continue(undefined);
|
||||||
await msg.complete({ ok: true });
|
}
|
||||||
return Loop.continue(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.name === "sandboxInstance.command.destroySession") {
|
if (msg.name === "sandboxInstance.command.destroySession") {
|
||||||
await loopCtx.step("sandbox-instance-destroy-session", async () =>
|
await loopCtx.step("sandbox-instance-destroy-session", async () => destroySessionMutation(loopCtx, msg.body as SessionControlCommand));
|
||||||
destroySessionMutation(loopCtx, msg.body as SessionControlCommand),
|
await msg.complete({ ok: true });
|
||||||
);
|
}
|
||||||
await msg.complete({ ok: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Loop.continue(undefined);
|
return Loop.continue(undefined);
|
||||||
});
|
});
|
||||||
|
|
@ -588,10 +556,14 @@ export const sandboxInstance = actor({
|
||||||
|
|
||||||
async destroy(c): Promise<void> {
|
async destroy(c): Promise<void> {
|
||||||
const self = selfSandboxInstance(c);
|
const self = selfSandboxInstance(c);
|
||||||
await self.send(sandboxInstanceWorkflowQueueName("sandboxInstance.command.destroy"), {}, {
|
await self.send(
|
||||||
wait: true,
|
sandboxInstanceWorkflowQueueName("sandboxInstance.command.destroy"),
|
||||||
timeout: 60_000,
|
{},
|
||||||
});
|
{
|
||||||
|
wait: true,
|
||||||
|
timeout: 60_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async createSession(c: any, command: CreateSessionCommand): Promise<CreateSessionResult> {
|
async createSession(c: any, command: CreateSessionCommand): Promise<CreateSessionResult> {
|
||||||
|
|
@ -604,10 +576,7 @@ export const sandboxInstance = actor({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async listSessions(
|
async listSessions(c: any, command?: ListSessionsCommand): Promise<{ items: SessionRecord[]; nextCursor?: string }> {
|
||||||
c: any,
|
|
||||||
command?: ListSessionsCommand
|
|
||||||
): Promise<{ items: SessionRecord[]; nextCursor?: string }> {
|
|
||||||
const persist = new SandboxInstancePersistDriver(c.db);
|
const persist = new SandboxInstancePersistDriver(c.db);
|
||||||
try {
|
try {
|
||||||
const client = await getSandboxAgentClient(c);
|
const client = await getSandboxAgentClient(c);
|
||||||
|
|
@ -626,7 +595,7 @@ export const sandboxInstance = actor({
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
providerId: c.state.providerId,
|
providerId: c.state.providerId,
|
||||||
sandboxId: c.state.sandboxId,
|
sandboxId: c.state.sandboxId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
return await persist.listSessions({
|
return await persist.listSessions({
|
||||||
cursor: command?.cursor,
|
cursor: command?.cursor,
|
||||||
|
|
@ -635,10 +604,7 @@ export const sandboxInstance = actor({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async listSessionEvents(
|
async listSessionEvents(c: any, command: ListSessionEventsCommand): Promise<{ items: SessionEvent[]; nextCursor?: string }> {
|
||||||
c: any,
|
|
||||||
command: ListSessionEventsCommand
|
|
||||||
): Promise<{ items: SessionEvent[]; nextCursor?: string }> {
|
|
||||||
const persist = new SandboxInstancePersistDriver(c.db);
|
const persist = new SandboxInstancePersistDriver(c.db);
|
||||||
return await persist.listEvents({
|
return await persist.listEvents({
|
||||||
sessionId: command.sessionId,
|
sessionId: command.sessionId,
|
||||||
|
|
@ -671,15 +637,9 @@ export const sandboxInstance = actor({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async sessionStatus(
|
async sessionStatus(c, command: SessionStatusCommand): Promise<{ id: string; status: "running" | "idle" | "error" }> {
|
||||||
c,
|
return await derivePersistedSessionStatus(new SandboxInstancePersistDriver(c.db), command.sessionId);
|
||||||
command: SessionStatusCommand
|
},
|
||||||
): Promise<{ id: string; status: "running" | "idle" | "error" }> {
|
|
||||||
return await derivePersistedSessionStatus(
|
|
||||||
new SandboxInstancePersistDriver(c.db),
|
|
||||||
command.sessionId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
run: workflow(runSandboxInstanceWorkflow),
|
run: workflow(runSandboxInstanceWorkflow),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
import { and, asc, count, eq } from "drizzle-orm";
|
import { and, asc, count, eq } from "drizzle-orm";
|
||||||
import type {
|
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from "sandbox-agent";
|
||||||
ListEventsRequest,
|
|
||||||
ListPage,
|
|
||||||
ListPageRequest,
|
|
||||||
SessionEvent,
|
|
||||||
SessionPersistDriver,
|
|
||||||
SessionRecord
|
|
||||||
} from "sandbox-agent";
|
|
||||||
import { sandboxSessionEvents, sandboxSessions } from "./db/schema.js";
|
import { sandboxSessionEvents, sandboxSessions } from "./db/schema.js";
|
||||||
|
|
||||||
const DEFAULT_MAX_SESSIONS = 1024;
|
const DEFAULT_MAX_SESSIONS = 1024;
|
||||||
|
|
@ -27,11 +20,7 @@ function parseCursor(cursor: string | undefined): number {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveEventListOffset(params: {
|
export function resolveEventListOffset(params: { cursor?: string; total: number; limit: number }): number {
|
||||||
cursor?: string;
|
|
||||||
total: number;
|
|
||||||
limit: number;
|
|
||||||
}): number {
|
|
||||||
if (params.cursor != null) {
|
if (params.cursor != null) {
|
||||||
return parseCursor(params.cursor);
|
return parseCursor(params.cursor);
|
||||||
}
|
}
|
||||||
|
|
@ -65,13 +54,10 @@ export class SandboxInstancePersistDriver implements SessionPersistDriver {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly db: any,
|
private readonly db: any,
|
||||||
options: SandboxInstancePersistDriverOptions = {}
|
options: SandboxInstancePersistDriverOptions = {},
|
||||||
) {
|
) {
|
||||||
this.maxSessions = normalizeCap(options.maxSessions, DEFAULT_MAX_SESSIONS);
|
this.maxSessions = normalizeCap(options.maxSessions, DEFAULT_MAX_SESSIONS);
|
||||||
this.maxEventsPerSession = normalizeCap(
|
this.maxEventsPerSession = normalizeCap(options.maxEventsPerSession, DEFAULT_MAX_EVENTS_PER_SESSION);
|
||||||
options.maxEventsPerSession,
|
|
||||||
DEFAULT_MAX_EVENTS_PER_SESSION
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSession(id: string): Promise<SessionRecord | null> {
|
async getSession(id: string): Promise<SessionRecord | null> {
|
||||||
|
|
@ -132,10 +118,7 @@ export class SandboxInstancePersistDriver implements SessionPersistDriver {
|
||||||
sessionInit: safeParseJson(row.sessionInitJson, undefined),
|
sessionInit: safeParseJson(row.sessionInitJson, undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const totalRow = await this.db
|
const totalRow = await this.db.select({ c: count() }).from(sandboxSessions).get();
|
||||||
.select({ c: count() })
|
|
||||||
.from(sandboxSessions)
|
|
||||||
.get();
|
|
||||||
const total = Number(totalRow?.c ?? 0);
|
const total = Number(totalRow?.c ?? 0);
|
||||||
|
|
||||||
const nextOffset = offset + items.length;
|
const nextOffset = offset + items.length;
|
||||||
|
|
@ -172,10 +155,7 @@ export class SandboxInstancePersistDriver implements SessionPersistDriver {
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
// Evict oldest sessions beyond cap.
|
// Evict oldest sessions beyond cap.
|
||||||
const totalRow = await this.db
|
const totalRow = await this.db.select({ c: count() }).from(sandboxSessions).get();
|
||||||
.select({ c: count() })
|
|
||||||
.from(sandboxSessions)
|
|
||||||
.get();
|
|
||||||
const total = Number(totalRow?.c ?? 0);
|
const total = Number(totalRow?.c ?? 0);
|
||||||
const overflow = total - this.maxSessions;
|
const overflow = total - this.maxSessions;
|
||||||
if (overflow <= 0) return;
|
if (overflow <= 0) return;
|
||||||
|
|
@ -195,11 +175,7 @@ export class SandboxInstancePersistDriver implements SessionPersistDriver {
|
||||||
|
|
||||||
async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {
|
async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {
|
||||||
const limit = normalizeCap(request.limit, DEFAULT_LIST_LIMIT);
|
const limit = normalizeCap(request.limit, DEFAULT_LIST_LIMIT);
|
||||||
const totalRow = await this.db
|
const totalRow = await this.db.select({ c: count() }).from(sandboxSessionEvents).where(eq(sandboxSessionEvents.sessionId, request.sessionId)).get();
|
||||||
.select({ c: count() })
|
|
||||||
.from(sandboxSessionEvents)
|
|
||||||
.where(eq(sandboxSessionEvents.sessionId, request.sessionId))
|
|
||||||
.get();
|
|
||||||
const total = Number(totalRow?.c ?? 0);
|
const total = Number(totalRow?.c ?? 0);
|
||||||
const offset = resolveEventListOffset({
|
const offset = resolveEventListOffset({
|
||||||
cursor: request.cursor,
|
cursor: request.cursor,
|
||||||
|
|
@ -267,11 +243,7 @@ export class SandboxInstancePersistDriver implements SessionPersistDriver {
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
// Trim oldest events beyond cap.
|
// Trim oldest events beyond cap.
|
||||||
const totalRow = await this.db
|
const totalRow = await this.db.select({ c: count() }).from(sandboxSessionEvents).where(eq(sandboxSessionEvents.sessionId, event.sessionId)).get();
|
||||||
.select({ c: count() })
|
|
||||||
.from(sandboxSessionEvents)
|
|
||||||
.where(eq(sandboxSessionEvents.sessionId, event.sessionId))
|
|
||||||
.get();
|
|
||||||
const total = Number(totalRow?.c ?? 0);
|
const total = Number(totalRow?.c ?? 0);
|
||||||
const overflow = total - this.maxEventsPerSession;
|
const overflow = total - this.maxEventsPerSession;
|
||||||
if (overflow <= 0) return;
|
if (overflow <= 0) return;
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,10 @@ import type {
|
||||||
RepoStackActionInput,
|
RepoStackActionInput,
|
||||||
RepoStackActionResult,
|
RepoStackActionResult,
|
||||||
RepoRecord,
|
RepoRecord,
|
||||||
|
StarSandboxAgentRepoInput,
|
||||||
|
StarSandboxAgentRepoResult,
|
||||||
SwitchResult,
|
SwitchResult,
|
||||||
WorkspaceUseInput
|
WorkspaceUseInput,
|
||||||
} from "@openhandoff/shared";
|
} from "@openhandoff/shared";
|
||||||
import { getActorRuntimeContext } from "../context.js";
|
import { getActorRuntimeContext } from "../context.js";
|
||||||
import { getHandoff, getOrCreateHistory, getOrCreateProject, selfWorkspace } from "../handles.js";
|
import { getHandoff, getOrCreateHistory, getOrCreateProject, selfWorkspace } from "../handles.js";
|
||||||
|
|
@ -58,11 +60,8 @@ interface RepoOverviewInput {
|
||||||
repoId: string;
|
repoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_QUEUE_NAMES = [
|
const WORKSPACE_QUEUE_NAMES = ["workspace.command.addRepo", "workspace.command.createHandoff", "workspace.command.refreshProviderProfiles"] as const;
|
||||||
"workspace.command.addRepo",
|
const SANDBOX_AGENT_REPO = "rivet-dev/sandbox-agent";
|
||||||
"workspace.command.createHandoff",
|
|
||||||
"workspace.command.refreshProviderProfiles",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type WorkspaceQueueName = (typeof WORKSPACE_QUEUE_NAMES)[number];
|
type WorkspaceQueueName = (typeof WORKSPACE_QUEUE_NAMES)[number];
|
||||||
|
|
||||||
|
|
@ -79,11 +78,7 @@ function assertWorkspace(c: { state: WorkspaceState }, workspaceId: string): voi
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveRepoId(c: any, handoffId: string): Promise<string> {
|
async function resolveRepoId(c: any, handoffId: string): Promise<string> {
|
||||||
const row = await c.db
|
const row = await c.db.select({ repoId: handoffLookup.repoId }).from(handoffLookup).where(eq(handoffLookup.handoffId, handoffId)).get();
|
||||||
.select({ repoId: handoffLookup.repoId })
|
|
||||||
.from(handoffLookup)
|
|
||||||
.where(eq(handoffLookup.handoffId, handoffId))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error(`Unknown handoff: ${handoffId} (not in lookup)`);
|
throw new Error(`Unknown handoff: ${handoffId} (not in lookup)`);
|
||||||
|
|
@ -107,11 +102,7 @@ async function upsertHandoffLookupRow(c: any, handoffId: string, repoId: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectAllHandoffSummaries(c: any): Promise<HandoffSummary[]> {
|
async function collectAllHandoffSummaries(c: any): Promise<HandoffSummary[]> {
|
||||||
const repoRows = await c.db
|
const repoRows = await c.db.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl }).from(repos).orderBy(desc(repos.updatedAt)).all();
|
||||||
.select({ repoId: repos.repoId, remoteUrl: repos.remoteUrl })
|
|
||||||
.from(repos)
|
|
||||||
.orderBy(desc(repos.updatedAt))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
const all: HandoffSummary[] = [];
|
const all: HandoffSummary[] = [];
|
||||||
for (const row of repoRows) {
|
for (const row of repoRows) {
|
||||||
|
|
@ -123,7 +114,7 @@ async function collectAllHandoffSummaries(c: any): Promise<HandoffSummary[]> {
|
||||||
logActorWarning("workspace", "failed collecting handoffs for repo", {
|
logActorWarning("workspace", "failed collecting handoffs for repo", {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: row.repoId,
|
repoId: row.repoId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +163,7 @@ async function buildWorkbenchSnapshot(c: any): Promise<HandoffWorkbenchSnapshot>
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: row.repoId,
|
repoId: row.repoId,
|
||||||
handoffId: summary.handoffId,
|
handoffId: summary.handoffId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +180,7 @@ async function buildWorkbenchSnapshot(c: any): Promise<HandoffWorkbenchSnapshot>
|
||||||
logActorWarning("workspace", "failed collecting workbench repo snapshot", {
|
logActorWarning("workspace", "failed collecting workbench repo snapshot", {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: row.repoId,
|
repoId: row.repoId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +191,7 @@ async function buildWorkbenchSnapshot(c: any): Promise<HandoffWorkbenchSnapshot>
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repos: repoRows.map((row) => ({
|
repos: repoRows.map((row) => ({
|
||||||
id: row.repoId,
|
id: row.repoId,
|
||||||
label: repoLabelFromRemote(row.remoteUrl)
|
label: repoLabelFromRemote(row.remoteUrl),
|
||||||
})),
|
})),
|
||||||
projects,
|
projects,
|
||||||
handoffs,
|
handoffs,
|
||||||
|
|
@ -232,14 +223,14 @@ async function addRepoMutation(c: any, input: AddRepoInput): Promise<RepoRecord>
|
||||||
repoId,
|
repoId,
|
||||||
remoteUrl,
|
remoteUrl,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: repos.repoId,
|
target: repos.repoId,
|
||||||
set: {
|
set: {
|
||||||
remoteUrl,
|
remoteUrl,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -249,7 +240,7 @@ async function addRepoMutation(c: any, input: AddRepoInput): Promise<RepoRecord>
|
||||||
repoId,
|
repoId,
|
||||||
remoteUrl,
|
remoteUrl,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,11 +251,7 @@ async function createHandoffMutation(c: any, input: CreateHandoffInput): Promise
|
||||||
const providerId = input.providerId ?? providers.defaultProviderId();
|
const providerId = input.providerId ?? providers.defaultProviderId();
|
||||||
|
|
||||||
const repoId = input.repoId;
|
const repoId = input.repoId;
|
||||||
const repoRow = await c.db
|
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, repoId)).get();
|
||||||
.select({ remoteUrl: repos.remoteUrl })
|
|
||||||
.from(repos)
|
|
||||||
.where(eq(repos.repoId, repoId))
|
|
||||||
.get();
|
|
||||||
if (!repoRow) {
|
if (!repoRow) {
|
||||||
throw new Error(`Unknown repo: ${repoId}`);
|
throw new Error(`Unknown repo: ${repoId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -275,14 +262,14 @@ async function createHandoffMutation(c: any, input: CreateHandoffInput): Promise
|
||||||
.values({
|
.values({
|
||||||
providerId,
|
providerId,
|
||||||
profileJson: JSON.stringify({ providerId }),
|
profileJson: JSON.stringify({ providerId }),
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: providerProfiles.providerId,
|
target: providerProfiles.providerId,
|
||||||
set: {
|
set: {
|
||||||
profileJson: JSON.stringify({ providerId }),
|
profileJson: JSON.stringify({ providerId }),
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -295,18 +282,18 @@ async function createHandoffMutation(c: any, input: CreateHandoffInput): Promise
|
||||||
agentType: input.agentType ?? null,
|
agentType: input.agentType ?? null,
|
||||||
explicitTitle: input.explicitTitle ?? null,
|
explicitTitle: input.explicitTitle ?? null,
|
||||||
explicitBranchName: input.explicitBranchName ?? null,
|
explicitBranchName: input.explicitBranchName ?? null,
|
||||||
onBranch: input.onBranch ?? null
|
onBranch: input.onBranch ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await c.db
|
await c.db
|
||||||
.insert(handoffLookup)
|
.insert(handoffLookup)
|
||||||
.values({
|
.values({
|
||||||
handoffId: created.handoffId,
|
handoffId: created.handoffId,
|
||||||
repoId
|
repoId,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: handoffLookup.handoffId,
|
target: handoffLookup.handoffId,
|
||||||
set: { repoId }
|
set: { repoId },
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|
@ -328,14 +315,14 @@ async function refreshProviderProfilesMutation(c: any, command?: RefreshProvider
|
||||||
.values({
|
.values({
|
||||||
providerId,
|
providerId,
|
||||||
profileJson: JSON.stringify({ providerId }),
|
profileJson: JSON.stringify({ providerId }),
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: providerProfiles.providerId,
|
target: providerProfiles.providerId,
|
||||||
set: {
|
set: {
|
||||||
profileJson: JSON.stringify({ providerId }),
|
profileJson: JSON.stringify({ providerId }),
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
@ -406,7 +393,7 @@ export const workspaceActions = {
|
||||||
repoId: repos.repoId,
|
repoId: repos.repoId,
|
||||||
remoteUrl: repos.remoteUrl,
|
remoteUrl: repos.remoteUrl,
|
||||||
createdAt: repos.createdAt,
|
createdAt: repos.createdAt,
|
||||||
updatedAt: repos.updatedAt
|
updatedAt: repos.updatedAt,
|
||||||
})
|
})
|
||||||
.from(repos)
|
.from(repos)
|
||||||
.orderBy(desc(repos.updatedAt))
|
.orderBy(desc(repos.updatedAt))
|
||||||
|
|
@ -417,7 +404,7 @@ export const workspaceActions = {
|
||||||
repoId: row.repoId,
|
repoId: row.repoId,
|
||||||
remoteUrl: row.remoteUrl,
|
remoteUrl: row.remoteUrl,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt
|
updatedAt: row.updatedAt,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -431,6 +418,16 @@ export const workspaceActions = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async starSandboxAgentRepo(c: any, input: StarSandboxAgentRepoInput): Promise<StarSandboxAgentRepoResult> {
|
||||||
|
assertWorkspace(c, input.workspaceId);
|
||||||
|
const { driver } = getActorRuntimeContext();
|
||||||
|
await driver.github.starRepository(SANDBOX_AGENT_REPO);
|
||||||
|
return {
|
||||||
|
repo: SANDBOX_AGENT_REPO,
|
||||||
|
starredAt: Date.now(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
async getWorkbench(c: any, input: WorkspaceUseInput): Promise<HandoffWorkbenchSnapshot> {
|
async getWorkbench(c: any, input: WorkspaceUseInput): Promise<HandoffWorkbenchSnapshot> {
|
||||||
assertWorkspace(c, input.workspaceId);
|
assertWorkspace(c, input.workspaceId);
|
||||||
return await buildWorkbenchSnapshot(c);
|
return await buildWorkbenchSnapshot(c);
|
||||||
|
|
@ -447,7 +444,7 @@ export const workspaceActions = {
|
||||||
task: input.task,
|
task: input.task,
|
||||||
...(input.title ? { explicitTitle: input.title } : {}),
|
...(input.title ? { explicitTitle: input.title } : {}),
|
||||||
...(input.branch ? { explicitBranchName: input.branch } : {}),
|
...(input.branch ? { explicitBranchName: input.branch } : {}),
|
||||||
...(input.model ? { agentType: agentTypeForModel(input.model) } : {})
|
...(input.model ? { agentType: agentTypeForModel(input.model) } : {}),
|
||||||
});
|
});
|
||||||
return { handoffId: created.handoffId };
|
return { handoffId: created.handoffId };
|
||||||
},
|
},
|
||||||
|
|
@ -521,11 +518,7 @@ export const workspaceActions = {
|
||||||
assertWorkspace(c, input.workspaceId);
|
assertWorkspace(c, input.workspaceId);
|
||||||
|
|
||||||
if (input.repoId) {
|
if (input.repoId) {
|
||||||
const repoRow = await c.db
|
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, input.repoId)).get();
|
||||||
.select({ remoteUrl: repos.remoteUrl })
|
|
||||||
.from(repos)
|
|
||||||
.where(eq(repos.repoId, input.repoId))
|
|
||||||
.get();
|
|
||||||
if (!repoRow) {
|
if (!repoRow) {
|
||||||
throw new Error(`Unknown repo: ${input.repoId}`);
|
throw new Error(`Unknown repo: ${input.repoId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -540,11 +533,7 @@ export const workspaceActions = {
|
||||||
async getRepoOverview(c: any, input: RepoOverviewInput): Promise<RepoOverview> {
|
async getRepoOverview(c: any, input: RepoOverviewInput): Promise<RepoOverview> {
|
||||||
assertWorkspace(c, input.workspaceId);
|
assertWorkspace(c, input.workspaceId);
|
||||||
|
|
||||||
const repoRow = await c.db
|
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, input.repoId)).get();
|
||||||
.select({ remoteUrl: repos.remoteUrl })
|
|
||||||
.from(repos)
|
|
||||||
.where(eq(repos.repoId, input.repoId))
|
|
||||||
.get();
|
|
||||||
if (!repoRow) {
|
if (!repoRow) {
|
||||||
throw new Error(`Unknown repo: ${input.repoId}`);
|
throw new Error(`Unknown repo: ${input.repoId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -557,11 +546,7 @@ export const workspaceActions = {
|
||||||
async runRepoStackAction(c: any, input: RepoStackActionInput): Promise<RepoStackActionResult> {
|
async runRepoStackAction(c: any, input: RepoStackActionInput): Promise<RepoStackActionResult> {
|
||||||
assertWorkspace(c, input.workspaceId);
|
assertWorkspace(c, input.workspaceId);
|
||||||
|
|
||||||
const repoRow = await c.db
|
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, input.repoId)).get();
|
||||||
.select({ remoteUrl: repos.remoteUrl })
|
|
||||||
.from(repos)
|
|
||||||
.where(eq(repos.repoId, input.repoId))
|
|
||||||
.get();
|
|
||||||
if (!repoRow) {
|
if (!repoRow) {
|
||||||
throw new Error(`Unknown repo: ${input.repoId}`);
|
throw new Error(`Unknown repo: ${input.repoId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -571,7 +556,7 @@ export const workspaceActions = {
|
||||||
return await project.runRepoStackAction({
|
return await project.runRepoStackAction({
|
||||||
action: input.action,
|
action: input.action,
|
||||||
branchName: input.branchName,
|
branchName: input.branchName,
|
||||||
parentBranch: input.parentBranch
|
parentBranch: input.parentBranch,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -585,7 +570,7 @@ export const workspaceActions = {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
handoffId,
|
handoffId,
|
||||||
providerId: record.providerId,
|
providerId: record.providerId,
|
||||||
switchTarget: switched.switchTarget
|
switchTarget: switched.switchTarget,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -611,14 +596,14 @@ export const workspaceActions = {
|
||||||
const items = await hist.list({
|
const items = await hist.list({
|
||||||
branch: input.branch,
|
branch: input.branch,
|
||||||
handoffId: input.handoffId,
|
handoffId: input.handoffId,
|
||||||
limit
|
limit,
|
||||||
});
|
});
|
||||||
allEvents.push(...items);
|
allEvents.push(...items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logActorWarning("workspace", "history lookup failed for repo", {
|
logActorWarning("workspace", "history lookup failed for repo", {
|
||||||
workspaceId: c.state.workspaceId,
|
workspaceId: c.state.workspaceId,
|
||||||
repoId: row.repoId,
|
repoId: row.repoId,
|
||||||
error: resolveErrorMessage(error)
|
error: resolveErrorMessage(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -632,11 +617,7 @@ export const workspaceActions = {
|
||||||
|
|
||||||
const repoId = await resolveRepoId(c, input.handoffId);
|
const repoId = await resolveRepoId(c, input.handoffId);
|
||||||
|
|
||||||
const repoRow = await c.db
|
const repoRow = await c.db.select({ remoteUrl: repos.remoteUrl }).from(repos).where(eq(repos.repoId, repoId)).get();
|
||||||
.select({ remoteUrl: repos.remoteUrl })
|
|
||||||
.from(repos)
|
|
||||||
.where(eq(repos.repoId, repoId))
|
|
||||||
.get();
|
|
||||||
if (!repoRow) {
|
if (!repoRow) {
|
||||||
throw new Error(`Unknown repo: ${repoId}`);
|
throw new Error(`Unknown repo: ${repoId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -685,5 +666,5 @@ export const workspaceActions = {
|
||||||
const repoId = await resolveRepoId(c, input.handoffId);
|
const repoId = await resolveRepoId(c, input.handoffId);
|
||||||
const h = getHandoff(c, c.state.workspaceId, repoId, input.handoffId);
|
const h = getHandoff(c, c.state.workspaceId, repoId, input.handoffId);
|
||||||
await h.kill({ reason: input.reason });
|
await h.kill({ reason: input.reason });
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,3 @@ export default defineConfig({
|
||||||
out: "./src/actors/workspace/db/drizzle",
|
out: "./src/actors/workspace/db/drizzle",
|
||||||
schema: "./src/actors/workspace/db/schema.ts",
|
schema: "./src/actors/workspace/db/schema.ts",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,4 +84,4 @@
|
||||||
"internal": {
|
"internal": {
|
||||||
"indexes": {}
|
"indexes": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,26 @@
|
||||||
// Do not hand-edit this file.
|
// Do not hand-edit this file.
|
||||||
|
|
||||||
const journal = {
|
const journal = {
|
||||||
"entries": [
|
entries: [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
idx: 0,
|
||||||
"when": 1770924376525,
|
when: 1770924376525,
|
||||||
"tag": "0000_rare_iron_man",
|
tag: "0000_rare_iron_man",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
idx: 1,
|
||||||
"when": 1770947252912,
|
when: 1770947252912,
|
||||||
"tag": "0001_sleepy_lady_deathstrike",
|
tag: "0001_sleepy_lady_deathstrike",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 2,
|
idx: 2,
|
||||||
"when": 1772668800000,
|
when: 1772668800000,
|
||||||
"tag": "0002_tiny_silver_surfer",
|
tag: "0002_tiny_silver_surfer",
|
||||||
"breakpoints": true
|
breakpoints: true,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -46,5 +46,5 @@ export default {
|
||||||
\`repo_id\` text NOT NULL
|
\`repo_id\` text NOT NULL
|
||||||
);
|
);
|
||||||
`,
|
`,
|
||||||
} as const
|
} as const,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export const workspace = actor({
|
||||||
actionTimeout: 5 * 60_000,
|
actionTimeout: 5 * 60_000,
|
||||||
},
|
},
|
||||||
createState: (_c, workspaceId: string) => ({
|
createState: (_c, workspaceId: string) => ({
|
||||||
workspaceId
|
workspaceId,
|
||||||
}),
|
}),
|
||||||
actions: workspaceActions,
|
actions: workspaceActions,
|
||||||
run: workflow(runWorkspaceWorkflow),
|
run: workflow(runWorkspaceWorkflow),
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,8 @@ export interface ActorSqliteDbOptions<TSchema extends Record<string, unknown>> {
|
||||||
baseDir?: string;
|
baseDir?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function actorSqliteDb<TSchema extends Record<string, unknown>>(
|
export function actorSqliteDb<TSchema extends Record<string, unknown>>(options: ActorSqliteDbOptions<TSchema>): DatabaseProvider<any & RawAccess> {
|
||||||
options: ActorSqliteDbOptions<TSchema>
|
const isBunRuntime = typeof (globalThis as any).Bun !== "undefined" && typeof (process as any)?.versions?.bun === "string";
|
||||||
): DatabaseProvider<any & RawAccess> {
|
|
||||||
const isBunRuntime =
|
|
||||||
typeof (globalThis as any).Bun !== "undefined" && typeof (process as any)?.versions?.bun === "string";
|
|
||||||
|
|
||||||
// Backend tests run in a Node-ish Vitest environment where `bun:sqlite` and
|
// Backend tests run in a Node-ish Vitest environment where `bun:sqlite` and
|
||||||
// Bun's sqlite-backed Drizzle driver are not supported.
|
// Bun's sqlite-backed Drizzle driver are not supported.
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ import {
|
||||||
gitSpiceSyncRepo,
|
gitSpiceSyncRepo,
|
||||||
gitSpiceTrackBranch,
|
gitSpiceTrackBranch,
|
||||||
} from "./integrations/git-spice/index.js";
|
} from "./integrations/git-spice/index.js";
|
||||||
import { listPullRequests, createPr } from "./integrations/github/index.js";
|
import { listPullRequests, createPr, starRepository } from "./integrations/github/index.js";
|
||||||
import { SandboxAgentClient } from "./integrations/sandbox-agent/client.js";
|
import { SandboxAgentClient } from "./integrations/sandbox-agent/client.js";
|
||||||
import { DaytonaClient } from "./integrations/daytona/client.js";
|
import { DaytonaClient } from "./integrations/daytona/client.js";
|
||||||
|
|
||||||
|
|
@ -78,12 +78,8 @@ export interface StackDriver {
|
||||||
|
|
||||||
export interface GithubDriver {
|
export interface GithubDriver {
|
||||||
listPullRequests(repoPath: string): Promise<PullRequestSnapshot[]>;
|
listPullRequests(repoPath: string): Promise<PullRequestSnapshot[]>;
|
||||||
createPr(
|
createPr(repoPath: string, headBranch: string, title: string, body?: string): Promise<{ number: number; url: string }>;
|
||||||
repoPath: string,
|
starRepository(repoFullName: string): Promise<void>;
|
||||||
headBranch: string,
|
|
||||||
title: string,
|
|
||||||
body?: string
|
|
||||||
): Promise<{ number: number; url: string }>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SandboxAgentClientLike {
|
export interface SandboxAgentClientLike {
|
||||||
|
|
@ -162,6 +158,7 @@ export function createDefaultDriver(): BackendDriver {
|
||||||
github: {
|
github: {
|
||||||
listPullRequests,
|
listPullRequests,
|
||||||
createPr,
|
createPr,
|
||||||
|
starRepository,
|
||||||
},
|
},
|
||||||
sandboxAgent: {
|
sandboxAgent: {
|
||||||
createClient: (opts) => {
|
createClient: (opts) => {
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,8 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
config.providers.daytona.endpoint =
|
config.providers.daytona.endpoint = envFirst("HF_DAYTONA_ENDPOINT", "DAYTONA_ENDPOINT") ?? config.providers.daytona.endpoint;
|
||||||
envFirst("HF_DAYTONA_ENDPOINT", "DAYTONA_ENDPOINT") ?? config.providers.daytona.endpoint;
|
config.providers.daytona.apiKey = envFirst("HF_DAYTONA_API_KEY", "DAYTONA_API_KEY") ?? config.providers.daytona.apiKey;
|
||||||
config.providers.daytona.apiKey =
|
|
||||||
envFirst("HF_DAYTONA_API_KEY", "DAYTONA_API_KEY") ?? config.providers.daytona.apiKey;
|
|
||||||
|
|
||||||
const driver = createDefaultDriver();
|
const driver = createDefaultDriver();
|
||||||
const providers = createProviderRegistry(config, driver);
|
const providers = createProviderRegistry(config, driver);
|
||||||
|
|
@ -58,7 +56,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
||||||
allowHeaders: ["Content-Type", "Authorization", "x-rivet-token"],
|
allowHeaders: ["Content-Type", "Authorization", "x-rivet-token"],
|
||||||
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||||
exposeHeaders: ["Content-Type"],
|
exposeHeaders: ["Content-Type"],
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
"/api/rivet",
|
"/api/rivet",
|
||||||
|
|
@ -67,7 +65,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
||||||
allowHeaders: ["Content-Type", "Authorization", "x-rivet-token"],
|
allowHeaders: ["Content-Type", "Authorization", "x-rivet-token"],
|
||||||
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||||
exposeHeaders: ["Content-Type"],
|
exposeHeaders: ["Content-Type"],
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
const forward = async (c: any) => {
|
const forward = async (c: any) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -86,7 +84,7 @@ export async function startBackend(options: BackendStartOptions = {}): Promise<v
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
hostname: config.backend.host,
|
hostname: config.backend.host,
|
||||||
port: config.backend.port
|
port: config.backend.port,
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", async () => {
|
process.on("SIGINT", async () => {
|
||||||
|
|
@ -130,13 +128,13 @@ async function main(): Promise<void> {
|
||||||
const port = parseArg("--port") ?? process.env.PORT ?? process.env.HF_BACKEND_PORT;
|
const port = parseArg("--port") ?? process.env.PORT ?? process.env.HF_BACKEND_PORT;
|
||||||
await startBackend({
|
await startBackend({
|
||||||
host,
|
host,
|
||||||
port: parseEnvPort(port)
|
port: parseEnvPort(port),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
main().catch((err: unknown) => {
|
main().catch((err: unknown) => {
|
||||||
const message = err instanceof Error ? err.stack ?? err.message : String(err);
|
const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
||||||
console.error(message);
|
console.error(message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,7 @@ export class DaytonaClient {
|
||||||
image: options.image,
|
image: options.image,
|
||||||
envVars: options.envVars,
|
envVars: options.envVars,
|
||||||
labels: options.labels,
|
labels: options.labels,
|
||||||
...(options.autoStopInterval !== undefined
|
...(options.autoStopInterval !== undefined ? { autoStopInterval: options.autoStopInterval } : {}),
|
||||||
? { autoStopInterval: options.autoStopInterval }
|
|
||||||
: {}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -32,18 +32,10 @@ function commandLabel(cmd: SpiceCommand): string {
|
||||||
|
|
||||||
function looksMissing(error: unknown): boolean {
|
function looksMissing(error: unknown): boolean {
|
||||||
const detail = error instanceof Error ? error.message : String(error);
|
const detail = error instanceof Error ? error.message : String(error);
|
||||||
return (
|
return detail.includes("ENOENT") || detail.includes("not a git command") || detail.includes("command not found");
|
||||||
detail.includes("ENOENT") ||
|
|
||||||
detail.includes("not a git command") ||
|
|
||||||
detail.includes("command not found")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryRun(
|
async function tryRun(repoPath: string, cmd: SpiceCommand, args: string[]): Promise<{ stdout: string; stderr: string }> {
|
||||||
repoPath: string,
|
|
||||||
cmd: SpiceCommand,
|
|
||||||
args: string[]
|
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
|
||||||
return await execFileAsync(cmd.command, [...cmd.prefix, ...args], {
|
return await execFileAsync(cmd.command, [...cmd.prefix, ...args], {
|
||||||
cwd: repoPath,
|
cwd: repoPath,
|
||||||
timeout: DEFAULT_TIMEOUT_MS,
|
timeout: DEFAULT_TIMEOUT_MS,
|
||||||
|
|
@ -51,8 +43,8 @@ async function tryRun(
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
NO_COLOR: "1",
|
NO_COLOR: "1",
|
||||||
FORCE_COLOR: "0"
|
FORCE_COLOR: "0",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,14 +132,7 @@ export async function gitSpiceAvailable(repoPath: string): Promise<boolean> {
|
||||||
|
|
||||||
export async function gitSpiceListStack(repoPath: string): Promise<SpiceStackEntry[]> {
|
export async function gitSpiceListStack(repoPath: string): Promise<SpiceStackEntry[]> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await runSpice(repoPath, [
|
const { stdout } = await runSpice(repoPath, ["log", "short", "--all", "--json", "--no-cr-status", "--no-prompt"]);
|
||||||
"log",
|
|
||||||
"short",
|
|
||||||
"--all",
|
|
||||||
"--json",
|
|
||||||
"--no-cr-status",
|
|
||||||
"--no-prompt"
|
|
||||||
]);
|
|
||||||
return parseLogJson(stdout);
|
return parseLogJson(stdout);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -160,9 +145,9 @@ export async function gitSpiceSyncRepo(repoPath: string): Promise<void> {
|
||||||
[
|
[
|
||||||
["repo", "sync", "--restack", "--no-prompt"],
|
["repo", "sync", "--restack", "--no-prompt"],
|
||||||
["repo", "sync", "--restack"],
|
["repo", "sync", "--restack"],
|
||||||
["repo", "sync"]
|
["repo", "sync"],
|
||||||
],
|
],
|
||||||
"git-spice repo sync failed"
|
"git-spice repo sync failed",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,9 +156,9 @@ export async function gitSpiceRestackRepo(repoPath: string): Promise<void> {
|
||||||
repoPath,
|
repoPath,
|
||||||
[
|
[
|
||||||
["repo", "restack", "--no-prompt"],
|
["repo", "restack", "--no-prompt"],
|
||||||
["repo", "restack"]
|
["repo", "restack"],
|
||||||
],
|
],
|
||||||
"git-spice repo restack failed"
|
"git-spice repo restack failed",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,9 +169,9 @@ export async function gitSpiceRestackSubtree(repoPath: string, branchName: strin
|
||||||
["upstack", "restack", "--branch", branchName, "--no-prompt"],
|
["upstack", "restack", "--branch", branchName, "--no-prompt"],
|
||||||
["upstack", "restack", "--branch", branchName],
|
["upstack", "restack", "--branch", branchName],
|
||||||
["branch", "restack", "--branch", branchName, "--no-prompt"],
|
["branch", "restack", "--branch", branchName, "--no-prompt"],
|
||||||
["branch", "restack", "--branch", branchName]
|
["branch", "restack", "--branch", branchName],
|
||||||
],
|
],
|
||||||
`git-spice restack subtree failed for ${branchName}`
|
`git-spice restack subtree failed for ${branchName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,41 +180,33 @@ export async function gitSpiceRebaseBranch(repoPath: string, branchName: string)
|
||||||
repoPath,
|
repoPath,
|
||||||
[
|
[
|
||||||
["branch", "restack", "--branch", branchName, "--no-prompt"],
|
["branch", "restack", "--branch", branchName, "--no-prompt"],
|
||||||
["branch", "restack", "--branch", branchName]
|
["branch", "restack", "--branch", branchName],
|
||||||
],
|
],
|
||||||
`git-spice branch restack failed for ${branchName}`
|
`git-spice branch restack failed for ${branchName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitSpiceReparentBranch(
|
export async function gitSpiceReparentBranch(repoPath: string, branchName: string, parentBranch: string): Promise<void> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string,
|
|
||||||
parentBranch: string
|
|
||||||
): Promise<void> {
|
|
||||||
await runFallbacks(
|
await runFallbacks(
|
||||||
repoPath,
|
repoPath,
|
||||||
[
|
[
|
||||||
["upstack", "onto", "--branch", branchName, parentBranch, "--no-prompt"],
|
["upstack", "onto", "--branch", branchName, parentBranch, "--no-prompt"],
|
||||||
["upstack", "onto", "--branch", branchName, parentBranch],
|
["upstack", "onto", "--branch", branchName, parentBranch],
|
||||||
["branch", "onto", "--branch", branchName, parentBranch, "--no-prompt"],
|
["branch", "onto", "--branch", branchName, parentBranch, "--no-prompt"],
|
||||||
["branch", "onto", "--branch", branchName, parentBranch]
|
["branch", "onto", "--branch", branchName, parentBranch],
|
||||||
],
|
],
|
||||||
`git-spice reparent failed for ${branchName} -> ${parentBranch}`
|
`git-spice reparent failed for ${branchName} -> ${parentBranch}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gitSpiceTrackBranch(
|
export async function gitSpiceTrackBranch(repoPath: string, branchName: string, parentBranch: string): Promise<void> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string,
|
|
||||||
parentBranch: string
|
|
||||||
): Promise<void> {
|
|
||||||
await runFallbacks(
|
await runFallbacks(
|
||||||
repoPath,
|
repoPath,
|
||||||
[
|
[
|
||||||
["branch", "track", branchName, "--base", parentBranch, "--no-prompt"],
|
["branch", "track", branchName, "--base", parentBranch, "--no-prompt"],
|
||||||
["branch", "track", branchName, "--base", parentBranch]
|
["branch", "track", branchName, "--base", parentBranch],
|
||||||
],
|
],
|
||||||
`git-spice track failed for ${branchName}`
|
`git-spice track failed for ${branchName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,7 @@ const DEFAULT_GIT_FETCH_TIMEOUT_MS = 2 * 60_000;
|
||||||
const DEFAULT_GIT_CLONE_TIMEOUT_MS = 5 * 60_000;
|
const DEFAULT_GIT_CLONE_TIMEOUT_MS = 5 * 60_000;
|
||||||
|
|
||||||
function resolveGithubToken(): string | null {
|
function resolveGithubToken(): string | null {
|
||||||
const token =
|
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? process.env.HF_GITHUB_TOKEN ?? process.env.HF_GH_TOKEN ?? null;
|
||||||
process.env.GH_TOKEN ??
|
|
||||||
process.env.GITHUB_TOKEN ??
|
|
||||||
process.env.HF_GITHUB_TOKEN ??
|
|
||||||
process.env.HF_GH_TOKEN ??
|
|
||||||
null;
|
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
const trimmed = token.trim();
|
const trimmed = token.trim();
|
||||||
return trimmed.length > 0 ? trimmed : null;
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
|
|
@ -33,19 +28,18 @@ function ensureAskpassScript(): string {
|
||||||
|
|
||||||
// Git invokes $GIT_ASKPASS with the prompt string as argv[1]. Provide both username and password.
|
// Git invokes $GIT_ASKPASS with the prompt string as argv[1]. Provide both username and password.
|
||||||
// We avoid embedding the token in this file; it is read from env at runtime.
|
// We avoid embedding the token in this file; it is read from env at runtime.
|
||||||
const content =
|
const content = [
|
||||||
[
|
"#!/bin/sh",
|
||||||
"#!/bin/sh",
|
'prompt="$1"',
|
||||||
'prompt="$1"',
|
// Prefer GH_TOKEN/GITHUB_TOKEN but support HF_* aliases too.
|
||||||
// Prefer GH_TOKEN/GITHUB_TOKEN but support HF_* aliases too.
|
'token="${GH_TOKEN:-${GITHUB_TOKEN:-${HF_GITHUB_TOKEN:-${HF_GH_TOKEN:-}}}}"',
|
||||||
'token="${GH_TOKEN:-${GITHUB_TOKEN:-${HF_GITHUB_TOKEN:-${HF_GH_TOKEN:-}}}}"',
|
'case "$prompt" in',
|
||||||
'case "$prompt" in',
|
' *Username*) echo "x-access-token" ;;',
|
||||||
' *Username*) echo "x-access-token" ;;',
|
' *Password*) echo "$token" ;;',
|
||||||
' *Password*) echo "$token" ;;',
|
' *) echo "" ;;',
|
||||||
' *) echo "" ;;',
|
"esac",
|
||||||
"esac",
|
"",
|
||||||
"",
|
].join("\n");
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
writeFileSync(path, content, "utf8");
|
writeFileSync(path, content, "utf8");
|
||||||
chmodSync(path, 0o700);
|
chmodSync(path, 0o700);
|
||||||
|
|
@ -141,12 +135,7 @@ export async function ensureCloned(remoteUrl: string, targetPath: string): Promi
|
||||||
|
|
||||||
export async function remoteDefaultBaseRef(repoPath: string): Promise<string> {
|
export async function remoteDefaultBaseRef(repoPath: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync("git", [
|
const { stdout } = await execFileAsync("git", ["-C", repoPath, "symbolic-ref", "refs/remotes/origin/HEAD"], { env: gitEnv() });
|
||||||
"-C",
|
|
||||||
repoPath,
|
|
||||||
"symbolic-ref",
|
|
||||||
"refs/remotes/origin/HEAD",
|
|
||||||
], { env: gitEnv() });
|
|
||||||
const ref = stdout.trim(); // refs/remotes/origin/main
|
const ref = stdout.trim(); // refs/remotes/origin/main
|
||||||
const match = ref.match(/^refs\/remotes\/(.+)$/);
|
const match = ref.match(/^refs\/remotes\/(.+)$/);
|
||||||
if (match?.[1]) {
|
if (match?.[1]) {
|
||||||
|
|
@ -169,17 +158,10 @@ export async function remoteDefaultBaseRef(repoPath: string): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listRemoteBranches(repoPath: string): Promise<BranchSnapshot[]> {
|
export async function listRemoteBranches(repoPath: string): Promise<BranchSnapshot[]> {
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync("git", ["-C", repoPath, "for-each-ref", "--format=%(refname:short) %(objectname)", "refs/remotes/origin"], {
|
||||||
"git",
|
maxBuffer: 1024 * 1024,
|
||||||
[
|
env: gitEnv(),
|
||||||
"-C",
|
});
|
||||||
repoPath,
|
|
||||||
"for-each-ref",
|
|
||||||
"--format=%(refname:short) %(objectname)",
|
|
||||||
"refs/remotes/origin",
|
|
||||||
],
|
|
||||||
{ maxBuffer: 1024 * 1024, env: gitEnv() }
|
|
||||||
);
|
|
||||||
|
|
||||||
return stdout
|
return stdout
|
||||||
.trim()
|
.trim()
|
||||||
|
|
@ -191,24 +173,12 @@ export async function listRemoteBranches(repoPath: string): Promise<BranchSnapsh
|
||||||
const branchName = short.replace(/^origin\//, "");
|
const branchName = short.replace(/^origin\//, "");
|
||||||
return { branchName, commitSha: commitSha ?? "" };
|
return { branchName, commitSha: commitSha ?? "" };
|
||||||
})
|
})
|
||||||
.filter(
|
.filter((row) => row.branchName.length > 0 && row.branchName !== "HEAD" && row.branchName !== "origin" && row.commitSha.length > 0);
|
||||||
(row) =>
|
|
||||||
row.branchName.length > 0 &&
|
|
||||||
row.branchName !== "HEAD" &&
|
|
||||||
row.branchName !== "origin" &&
|
|
||||||
row.commitSha.length > 0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remoteBranchExists(repoPath: string, branchName: string): Promise<boolean> {
|
async function remoteBranchExists(repoPath: string, branchName: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await execFileAsync("git", [
|
await execFileAsync("git", ["-C", repoPath, "show-ref", "--verify", `refs/remotes/origin/${branchName}`], { env: gitEnv() });
|
||||||
"-C",
|
|
||||||
repoPath,
|
|
||||||
"show-ref",
|
|
||||||
"--verify",
|
|
||||||
`refs/remotes/origin/${branchName}`,
|
|
||||||
], { env: gitEnv() });
|
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -233,11 +203,10 @@ export async function diffStatForBranch(repoPath: string, branchName: string): P
|
||||||
try {
|
try {
|
||||||
const baseRef = await remoteDefaultBaseRef(repoPath);
|
const baseRef = await remoteDefaultBaseRef(repoPath);
|
||||||
const headRef = `origin/${branchName}`;
|
const headRef = `origin/${branchName}`;
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync("git", ["-C", repoPath, "diff", "--shortstat", `${baseRef}...${headRef}`], {
|
||||||
"git",
|
maxBuffer: 1024 * 1024,
|
||||||
["-C", repoPath, "diff", "--shortstat", `${baseRef}...${headRef}`],
|
env: gitEnv(),
|
||||||
{ maxBuffer: 1024 * 1024, env: gitEnv() }
|
});
|
||||||
);
|
|
||||||
const trimmed = stdout.trim();
|
const trimmed = stdout.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return "+0/-0";
|
return "+0/-0";
|
||||||
|
|
@ -252,20 +221,13 @@ export async function diffStatForBranch(repoPath: string, branchName: string): P
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function conflictsWithMain(
|
export async function conflictsWithMain(repoPath: string, branchName: string): Promise<boolean> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
const baseRef = await remoteDefaultBaseRef(repoPath);
|
const baseRef = await remoteDefaultBaseRef(repoPath);
|
||||||
const headRef = `origin/${branchName}`;
|
const headRef = `origin/${branchName}`;
|
||||||
// Use merge-tree (git 2.38+) for a clean conflict check.
|
// Use merge-tree (git 2.38+) for a clean conflict check.
|
||||||
try {
|
try {
|
||||||
await execFileAsync(
|
await execFileAsync("git", ["-C", repoPath, "merge-tree", "--write-tree", "--no-messages", baseRef, headRef], { env: gitEnv() });
|
||||||
"git",
|
|
||||||
["-C", repoPath, "merge-tree", "--write-tree", "--no-messages", baseRef, headRef],
|
|
||||||
{ env: gitEnv() }
|
|
||||||
);
|
|
||||||
// If merge-tree exits 0, no conflicts. Non-zero exit means conflicts.
|
// If merge-tree exits 0, no conflicts. Non-zero exit means conflicts.
|
||||||
return false;
|
return false;
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -279,11 +241,7 @@ export async function conflictsWithMain(
|
||||||
|
|
||||||
export async function getOriginOwner(repoPath: string): Promise<string> {
|
export async function getOriginOwner(repoPath: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync("git", ["-C", repoPath, "remote", "get-url", "origin"], { env: gitEnv() });
|
||||||
"git",
|
|
||||||
["-C", repoPath, "remote", "get-url", "origin"],
|
|
||||||
{ env: gitEnv() }
|
|
||||||
);
|
|
||||||
const url = stdout.trim();
|
const url = stdout.trim();
|
||||||
// Handle SSH: git@github.com:owner/repo.git
|
// Handle SSH: git@github.com:owner/repo.git
|
||||||
const sshMatch = url.match(/[:\/]([^\/]+)\/[^\/]+(?:\.git)?$/);
|
const sshMatch = url.match(/[:\/]([^\/]+)\/[^\/]+(?:\.git)?$/);
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,7 @@ interface GhPrListItem {
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCiStatus(
|
function parseCiStatus(checks: GhPrListItem["statusCheckRollup"]): string | null {
|
||||||
checks: GhPrListItem["statusCheckRollup"]
|
|
||||||
): string | null {
|
|
||||||
if (!checks || checks.length === 0) return null;
|
if (!checks || checks.length === 0) return null;
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
@ -53,12 +51,7 @@ function parseCiStatus(
|
||||||
|
|
||||||
if (conclusion === "SUCCESS" || state === "SUCCESS") {
|
if (conclusion === "SUCCESS" || state === "SUCCESS") {
|
||||||
successes++;
|
successes++;
|
||||||
} else if (
|
} else if (status === "IN_PROGRESS" || status === "QUEUED" || status === "PENDING" || state === "PENDING") {
|
||||||
status === "IN_PROGRESS" ||
|
|
||||||
status === "QUEUED" ||
|
|
||||||
status === "PENDING" ||
|
|
||||||
state === "PENDING"
|
|
||||||
) {
|
|
||||||
hasRunning = true;
|
hasRunning = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,9 +63,7 @@ function parseCiStatus(
|
||||||
return `${successes}/${total}`;
|
return `${successes}/${total}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseReviewStatus(
|
function parseReviewStatus(reviews: GhPrListItem["reviews"]): { status: string | null; reviewer: string | null } {
|
||||||
reviews: GhPrListItem["reviews"]
|
|
||||||
): { status: string | null; reviewer: string | null } {
|
|
||||||
if (!reviews || reviews.length === 0) {
|
if (!reviews || reviews.length === 0) {
|
||||||
return { status: null, reviewer: null };
|
return { status: null, reviewer: null };
|
||||||
}
|
}
|
||||||
|
|
@ -120,35 +111,21 @@ function snapshotFromGhItem(item: GhPrListItem): PullRequestSnapshot {
|
||||||
isDraft: item.isDraft ?? false,
|
isDraft: item.isDraft ?? false,
|
||||||
ciStatus: parseCiStatus(item.statusCheckRollup),
|
ciStatus: parseCiStatus(item.statusCheckRollup),
|
||||||
reviewStatus,
|
reviewStatus,
|
||||||
reviewer
|
reviewer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const PR_JSON_FIELDS =
|
const PR_JSON_FIELDS = "number,headRefName,state,title,url,author,isDraft,statusCheckRollup,reviews";
|
||||||
"number,headRefName,state,title,url,author,isDraft,statusCheckRollup,reviews";
|
|
||||||
|
|
||||||
export async function listPullRequests(repoPath: string): Promise<PullRequestSnapshot[]> {
|
export async function listPullRequests(repoPath: string): Promise<PullRequestSnapshot[]> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync("gh", ["pr", "list", "--json", PR_JSON_FIELDS, "--limit", "200"], { maxBuffer: 1024 * 1024 * 4, cwd: repoPath });
|
||||||
"gh",
|
|
||||||
[
|
|
||||||
"pr",
|
|
||||||
"list",
|
|
||||||
"--json",
|
|
||||||
PR_JSON_FIELDS,
|
|
||||||
"--limit",
|
|
||||||
"200"
|
|
||||||
],
|
|
||||||
{ maxBuffer: 1024 * 1024 * 4, cwd: repoPath }
|
|
||||||
);
|
|
||||||
|
|
||||||
const parsed = JSON.parse(stdout) as GhPrListItem[];
|
const parsed = JSON.parse(stdout) as GhPrListItem[];
|
||||||
|
|
||||||
return parsed.map((item) => {
|
return parsed.map((item) => {
|
||||||
// Handle fork PRs where headRefName may contain "owner:branch"
|
// Handle fork PRs where headRefName may contain "owner:branch"
|
||||||
const headRefName = item.headRefName.includes(":")
|
const headRefName = item.headRefName.includes(":") ? (item.headRefName.split(":").pop() ?? item.headRefName) : item.headRefName;
|
||||||
? item.headRefName.split(":").pop() ?? item.headRefName
|
|
||||||
: item.headRefName;
|
|
||||||
|
|
||||||
return snapshotFromGhItem({ ...item, headRefName });
|
return snapshotFromGhItem({ ...item, headRefName });
|
||||||
});
|
});
|
||||||
|
|
@ -157,22 +134,9 @@ export async function listPullRequests(repoPath: string): Promise<PullRequestSna
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPrInfo(
|
export async function getPrInfo(repoPath: string, branchName: string): Promise<PullRequestSnapshot | null> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<PullRequestSnapshot | null> {
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync("gh", ["pr", "view", branchName, "--json", PR_JSON_FIELDS], { maxBuffer: 1024 * 1024 * 4, cwd: repoPath });
|
||||||
"gh",
|
|
||||||
[
|
|
||||||
"pr",
|
|
||||||
"view",
|
|
||||||
branchName,
|
|
||||||
"--json",
|
|
||||||
PR_JSON_FIELDS
|
|
||||||
],
|
|
||||||
{ maxBuffer: 1024 * 1024 * 4, cwd: repoPath }
|
|
||||||
);
|
|
||||||
|
|
||||||
const item = JSON.parse(stdout) as GhPrListItem;
|
const item = JSON.parse(stdout) as GhPrListItem;
|
||||||
return snapshotFromGhItem(item);
|
return snapshotFromGhItem(item);
|
||||||
|
|
@ -181,12 +145,7 @@ export async function getPrInfo(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createPr(
|
export async function createPr(repoPath: string, headBranch: string, title: string, body?: string): Promise<{ number: number; url: string }> {
|
||||||
repoPath: string,
|
|
||||||
headBranch: string,
|
|
||||||
title: string,
|
|
||||||
body?: string
|
|
||||||
): Promise<{ number: number; url: string }> {
|
|
||||||
const args = ["pr", "create", "--title", title, "--head", headBranch];
|
const args = ["pr", "create", "--title", title, "--head", headBranch];
|
||||||
if (body) {
|
if (body) {
|
||||||
args.push("--body", body);
|
args.push("--body", body);
|
||||||
|
|
@ -196,7 +155,7 @@ export async function createPr(
|
||||||
|
|
||||||
const { stdout } = await execFileAsync("gh", args, {
|
const { stdout } = await execFileAsync("gh", args, {
|
||||||
maxBuffer: 1024 * 1024,
|
maxBuffer: 1024 * 1024,
|
||||||
cwd: repoPath
|
cwd: repoPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
// gh pr create outputs the PR URL on success
|
// gh pr create outputs the PR URL on success
|
||||||
|
|
@ -208,29 +167,29 @@ export async function createPr(
|
||||||
return { number, url };
|
return { number, url };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllowedMergeMethod(
|
export async function starRepository(repoFullName: string): Promise<void> {
|
||||||
repoPath: string
|
try {
|
||||||
): Promise<"squash" | "rebase" | "merge"> {
|
await execFileAsync("gh", ["api", "--method", "PUT", `user/starred/${repoFullName}`], {
|
||||||
|
maxBuffer: 1024 * 1024,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : `Failed to star GitHub repository ${repoFullName}. Ensure GitHub auth is configured for the backend.`;
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllowedMergeMethod(repoPath: string): Promise<"squash" | "rebase" | "merge"> {
|
||||||
try {
|
try {
|
||||||
// Get the repo owner/name from gh
|
// Get the repo owner/name from gh
|
||||||
const { stdout: repoJson } = await execFileAsync(
|
const { stdout: repoJson } = await execFileAsync("gh", ["repo", "view", "--json", "owner,name"], { cwd: repoPath });
|
||||||
"gh",
|
|
||||||
["repo", "view", "--json", "owner,name"],
|
|
||||||
{ cwd: repoPath }
|
|
||||||
);
|
|
||||||
const repo = JSON.parse(repoJson) as { owner: { login: string }; name: string };
|
const repo = JSON.parse(repoJson) as { owner: { login: string }; name: string };
|
||||||
const repoFullName = `${repo.owner.login}/${repo.name}`;
|
const repoFullName = `${repo.owner.login}/${repo.name}`;
|
||||||
|
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync("gh", ["api", `repos/${repoFullName}`, "--jq", ".allow_squash_merge, .allow_rebase_merge, .allow_merge_commit"], {
|
||||||
"gh",
|
maxBuffer: 1024 * 1024,
|
||||||
[
|
cwd: repoPath,
|
||||||
"api",
|
});
|
||||||
`repos/${repoFullName}`,
|
|
||||||
"--jq",
|
|
||||||
".allow_squash_merge, .allow_rebase_merge, .allow_merge_commit"
|
|
||||||
],
|
|
||||||
{ maxBuffer: 1024 * 1024, cwd: repoPath }
|
|
||||||
);
|
|
||||||
|
|
||||||
const lines = stdout.trim().split("\n");
|
const lines = stdout.trim().split("\n");
|
||||||
const allowSquash = lines[0]?.trim() === "true";
|
const allowSquash = lines[0]?.trim() === "true";
|
||||||
|
|
@ -248,23 +207,12 @@ export async function getAllowedMergeMethod(
|
||||||
|
|
||||||
export async function mergePr(repoPath: string, prNumber: number): Promise<void> {
|
export async function mergePr(repoPath: string, prNumber: number): Promise<void> {
|
||||||
const method = await getAllowedMergeMethod(repoPath);
|
const method = await getAllowedMergeMethod(repoPath);
|
||||||
await execFileAsync(
|
await execFileAsync("gh", ["pr", "merge", String(prNumber), `--${method}`, "--delete-branch"], { cwd: repoPath });
|
||||||
"gh",
|
|
||||||
["pr", "merge", String(prNumber), `--${method}`, "--delete-branch"],
|
|
||||||
{ cwd: repoPath }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isPrMerged(
|
export async function isPrMerged(repoPath: string, branchName: string): Promise<boolean> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync("gh", ["pr", "view", branchName, "--json", "state"], { cwd: repoPath });
|
||||||
"gh",
|
|
||||||
["pr", "view", branchName, "--json", "state"],
|
|
||||||
{ cwd: repoPath }
|
|
||||||
);
|
|
||||||
const parsed = JSON.parse(stdout) as { state: string };
|
const parsed = JSON.parse(stdout) as { state: string };
|
||||||
return parsed.state.toUpperCase() === "MERGED";
|
return parsed.state.toUpperCase() === "MERGED";
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -272,16 +220,9 @@ export async function isPrMerged(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPrTitle(
|
export async function getPrTitle(repoPath: string, branchName: string): Promise<string | null> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(
|
const { stdout } = await execFileAsync("gh", ["pr", "view", branchName, "--json", "title"], { cwd: repoPath });
|
||||||
"gh",
|
|
||||||
["pr", "view", branchName, "--json", "title"],
|
|
||||||
{ cwd: repoPath }
|
|
||||||
);
|
|
||||||
const parsed = JSON.parse(stdout) as { title: string };
|
const parsed = JSON.parse(stdout) as { title: string };
|
||||||
return parsed.title;
|
return parsed.title;
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,11 @@ export async function graphiteGet(repoPath: string, branchName: string): Promise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function graphiteCreateBranch(
|
export async function graphiteCreateBranch(repoPath: string, branchName: string): Promise<void> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<void> {
|
|
||||||
await execFileAsync("gt", ["create", branchName], { cwd: repoPath });
|
await execFileAsync("gt", ["create", branchName], { cwd: repoPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function graphiteCheckout(
|
export async function graphiteCheckout(repoPath: string, branchName: string): Promise<void> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<void> {
|
|
||||||
await execFileAsync("gt", ["checkout", branchName], { cwd: repoPath });
|
await execFileAsync("gt", ["checkout", branchName], { cwd: repoPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,17 +33,11 @@ export async function graphiteSubmit(repoPath: string): Promise<void> {
|
||||||
await execFileAsync("gt", ["submit", "--no-edit"], { cwd: repoPath });
|
await execFileAsync("gt", ["submit", "--no-edit"], { cwd: repoPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function graphiteMergeBranch(
|
export async function graphiteMergeBranch(repoPath: string, branchName: string): Promise<void> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<void> {
|
|
||||||
await execFileAsync("gt", ["merge", branchName], { cwd: repoPath });
|
await execFileAsync("gt", ["merge", branchName], { cwd: repoPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function graphiteAbandon(
|
export async function graphiteAbandon(repoPath: string, branchName: string): Promise<void> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<void> {
|
|
||||||
await execFileAsync("gt", ["abandon", branchName], { cwd: repoPath });
|
await execFileAsync("gt", ["abandon", branchName], { cwd: repoPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,14 +46,12 @@ export interface GraphiteStackEntry {
|
||||||
parentBranch: string | null;
|
parentBranch: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function graphiteGetStack(
|
export async function graphiteGetStack(repoPath: string): Promise<GraphiteStackEntry[]> {
|
||||||
repoPath: string
|
|
||||||
): Promise<GraphiteStackEntry[]> {
|
|
||||||
try {
|
try {
|
||||||
// Try JSON output first
|
// Try JSON output first
|
||||||
const { stdout } = await execFileAsync("gt", ["log", "--json"], {
|
const { stdout } = await execFileAsync("gt", ["log", "--json"], {
|
||||||
cwd: repoPath,
|
cwd: repoPath,
|
||||||
maxBuffer: 1024 * 1024
|
maxBuffer: 1024 * 1024,
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(stdout) as Array<{
|
const parsed = JSON.parse(stdout) as Array<{
|
||||||
|
|
@ -77,14 +63,14 @@ export async function graphiteGetStack(
|
||||||
|
|
||||||
return parsed.map((entry) => ({
|
return parsed.map((entry) => ({
|
||||||
branchName: entry.branch ?? entry.name ?? "",
|
branchName: entry.branch ?? entry.name ?? "",
|
||||||
parentBranch: entry.parent ?? entry.parentBranch ?? null
|
parentBranch: entry.parent ?? entry.parentBranch ?? null,
|
||||||
}));
|
}));
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to text parsing of `gt log`
|
// Fall back to text parsing of `gt log`
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync("gt", ["log"], {
|
const { stdout } = await execFileAsync("gt", ["log"], {
|
||||||
cwd: repoPath,
|
cwd: repoPath,
|
||||||
maxBuffer: 1024 * 1024
|
maxBuffer: 1024 * 1024,
|
||||||
});
|
});
|
||||||
|
|
||||||
const entries: GraphiteStackEntry[] = [];
|
const entries: GraphiteStackEntry[] = [];
|
||||||
|
|
@ -113,9 +99,7 @@ export async function graphiteGetStack(
|
||||||
branchStack.pop();
|
branchStack.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentBranch = branchStack.length > 0
|
const parentBranch = branchStack.length > 0 ? (branchStack[branchStack.length - 1] ?? null) : null;
|
||||||
? branchStack[branchStack.length - 1] ?? null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
entries.push({ branchName, parentBranch });
|
entries.push({ branchName, parentBranch });
|
||||||
branchStack.push(branchName);
|
branchStack.push(branchName);
|
||||||
|
|
@ -128,15 +112,12 @@ export async function graphiteGetStack(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function graphiteGetParent(
|
export async function graphiteGetParent(repoPath: string, branchName: string): Promise<string | null> {
|
||||||
repoPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
try {
|
||||||
// Try `gt get <branchName>` to see parent info
|
// Try `gt get <branchName>` to see parent info
|
||||||
const { stdout } = await execFileAsync("gt", ["get", branchName], {
|
const { stdout } = await execFileAsync("gt", ["get", branchName], {
|
||||||
cwd: repoPath,
|
cwd: repoPath,
|
||||||
maxBuffer: 1024 * 1024
|
maxBuffer: 1024 * 1024,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parse output for parent branch reference
|
// Parse output for parent branch reference
|
||||||
|
|
|
||||||
|
|
@ -123,18 +123,11 @@ export class SandboxAgentClient {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
const lowered = message.toLowerCase();
|
const lowered = message.toLowerCase();
|
||||||
// sandbox-agent server times out long-running ACP prompts and returns a 504-like error.
|
// sandbox-agent server times out long-running ACP prompts and returns a 504-like error.
|
||||||
return (
|
return lowered.includes("timeout waiting for agent response") || lowered.includes("timed out waiting for agent response") || lowered.includes("504");
|
||||||
lowered.includes("timeout waiting for agent response") ||
|
|
||||||
lowered.includes("timed out waiting for agent response") ||
|
|
||||||
lowered.includes("504")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(request: string | SandboxSessionCreateRequest): Promise<SandboxSession> {
|
async createSession(request: string | SandboxSessionCreateRequest): Promise<SandboxSession> {
|
||||||
const normalized: SandboxSessionCreateRequest =
|
const normalized: SandboxSessionCreateRequest = typeof request === "string" ? { prompt: request } : request;
|
||||||
typeof request === "string"
|
|
||||||
? { prompt: request }
|
|
||||||
: request;
|
|
||||||
const sdk = await this.sdk();
|
const sdk = await this.sdk();
|
||||||
// Do not wrap createSession in a local Promise.race timeout. The underlying SDK
|
// Do not wrap createSession in a local Promise.race timeout. The underlying SDK
|
||||||
// call is not abortable, so local timeout races create overlapping ACP requests and
|
// call is not abortable, so local timeout races create overlapping ACP requests and
|
||||||
|
|
@ -381,18 +374,14 @@ export class SandboxAgentClient {
|
||||||
} while (cursor);
|
} while (cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateCommitMessage(
|
async generateCommitMessage(dir: string, spec: string, task: string): Promise<string> {
|
||||||
dir: string,
|
|
||||||
spec: string,
|
|
||||||
task: string
|
|
||||||
): Promise<string> {
|
|
||||||
const prompt = [
|
const prompt = [
|
||||||
"Generate a conventional commit message for the following changes.",
|
"Generate a conventional commit message for the following changes.",
|
||||||
"Return ONLY the commit message, no explanation or markdown formatting.",
|
"Return ONLY the commit message, no explanation or markdown formatting.",
|
||||||
"",
|
"",
|
||||||
`Task: ${task}`,
|
`Task: ${task}`,
|
||||||
"",
|
"",
|
||||||
`Spec/diff:\n${spec}`
|
`Spec/diff:\n${spec}`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
const sdk = await this.sdk();
|
const sdk = await this.sdk();
|
||||||
|
|
|
||||||
|
|
@ -99,10 +99,10 @@ export class TerminalBellBackend implements NotifyBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
const backendFactories: Record<string, () => NotifyBackend> = {
|
const backendFactories: Record<string, () => NotifyBackend> = {
|
||||||
"openclaw": () => new OpenclawBackend(),
|
openclaw: () => new OpenclawBackend(),
|
||||||
"macos-osascript": () => new MacOsNotifyBackend(),
|
"macos-osascript": () => new MacOsNotifyBackend(),
|
||||||
"linux-notify-send": () => new LinuxNotifySendBackend(),
|
"linux-notify-send": () => new LinuxNotifySendBackend(),
|
||||||
"terminal": () => new TerminalBellBackend(),
|
terminal: () => new TerminalBellBackend(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function createBackends(configOrder: string[]): Promise<NotifyBackend[]> {
|
export async function createBackends(configOrder: string[]): Promise<NotifyBackend[]> {
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,7 @@ export function createNotificationService(backends: NotifyBackend[]): Notificati
|
||||||
},
|
},
|
||||||
|
|
||||||
async changesRequested(branchName: string, prNumber: number, reviewer: string): Promise<void> {
|
async changesRequested(branchName: string, prNumber: number, reviewer: string): Promise<void> {
|
||||||
await notify(
|
await notify("Changes Requested", `Changes requested on PR #${prNumber} (${branchName}) by ${reviewer}`, "high");
|
||||||
"Changes Requested",
|
|
||||||
`Changes requested on PR #${prNumber} (${branchName}) by ${reviewer}`,
|
|
||||||
"high",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async prMerged(branchName: string, prNumber: number): Promise<void> {
|
async prMerged(branchName: string, prNumber: number): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,7 @@ export class PrStateTracker {
|
||||||
this.states = new Map();
|
this.states = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(repoId: string, branchName: string, prNumber: number, ci: CiState, review: ReviewState, reviewer?: string): PrStateTransition[] {
|
||||||
repoId: string,
|
|
||||||
branchName: string,
|
|
||||||
prNumber: number,
|
|
||||||
ci: CiState,
|
|
||||||
review: ReviewState,
|
|
||||||
reviewer?: string,
|
|
||||||
): PrStateTransition[] {
|
|
||||||
const key = `${repoId}:${branchName}`;
|
const key = `${repoId}:${branchName}`;
|
||||||
const prev = this.states.get(key);
|
const prev = this.states.get(key);
|
||||||
const transitions: PrStateTransition[] = [];
|
const transitions: PrStateTransition[] = [];
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import type {
|
||||||
SandboxHandle,
|
SandboxHandle,
|
||||||
SandboxHealth,
|
SandboxHealth,
|
||||||
SandboxHealthRequest,
|
SandboxHealthRequest,
|
||||||
SandboxProvider
|
SandboxProvider,
|
||||||
} from "../provider-api/index.js";
|
} from "../provider-api/index.js";
|
||||||
import type { DaytonaDriver } from "../../driver.js";
|
import type { DaytonaDriver } from "../../driver.js";
|
||||||
import { Image } from "@daytonaio/sdk";
|
import { Image } from "@daytonaio/sdk";
|
||||||
|
|
@ -33,7 +33,7 @@ export interface DaytonaProviderConfig {
|
||||||
export class DaytonaProvider implements SandboxProvider {
|
export class DaytonaProvider implements SandboxProvider {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: DaytonaProviderConfig,
|
private readonly config: DaytonaProviderConfig,
|
||||||
private readonly daytona?: DaytonaDriver
|
private readonly daytona?: DaytonaDriver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private static readonly SANDBOX_AGENT_PORT = 2468;
|
private static readonly SANDBOX_AGENT_PORT = 2468;
|
||||||
|
|
@ -60,10 +60,7 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAcpRequestTimeoutMs(): number {
|
private getAcpRequestTimeoutMs(): number {
|
||||||
const parsed = Number(
|
const parsed = Number(process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS ?? DaytonaProvider.DEFAULT_ACP_REQUEST_TIMEOUT_MS.toString());
|
||||||
process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS
|
|
||||||
?? DaytonaProvider.DEFAULT_ACP_REQUEST_TIMEOUT_MS.toString()
|
|
||||||
);
|
|
||||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
return DaytonaProvider.DEFAULT_ACP_REQUEST_TIMEOUT_MS;
|
return DaytonaProvider.DEFAULT_ACP_REQUEST_TIMEOUT_MS;
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +114,7 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"daytona provider is not configured: missing apiKey. " +
|
"daytona provider is not configured: missing apiKey. " +
|
||||||
"Set HF_DAYTONA_API_KEY (or DAYTONA_API_KEY). " +
|
"Set HF_DAYTONA_API_KEY (or DAYTONA_API_KEY). " +
|
||||||
"Optionally set HF_DAYTONA_ENDPOINT (or DAYTONA_ENDPOINT)."
|
"Optionally set HF_DAYTONA_ENDPOINT (or DAYTONA_ENDPOINT).",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,20 +151,14 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
return Image.base(this.config.image).runCommands(
|
return Image.base(this.config.image).runCommands(
|
||||||
"apt-get update && apt-get install -y curl ca-certificates git openssh-client nodejs npm",
|
"apt-get update && apt-get install -y curl ca-certificates git openssh-client nodejs npm",
|
||||||
`curl -fsSL https://releases.rivet.dev/sandbox-agent/${DaytonaProvider.SANDBOX_AGENT_VERSION}/install.sh | sh`,
|
`curl -fsSL https://releases.rivet.dev/sandbox-agent/${DaytonaProvider.SANDBOX_AGENT_VERSION}/install.sh | sh`,
|
||||||
`bash -lc 'export PATH="$HOME/.local/bin:$PATH"; sandbox-agent install-agent codex || true; sandbox-agent install-agent claude || true'`
|
`bash -lc 'export PATH="$HOME/.local/bin:$PATH"; sandbox-agent install-agent codex || true; sandbox-agent install-agent claude || true'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runCheckedCommand(
|
private async runCheckedCommand(sandboxId: string, command: string, label: string): Promise<void> {
|
||||||
sandboxId: string,
|
|
||||||
command: string,
|
|
||||||
label: string
|
|
||||||
): Promise<void> {
|
|
||||||
const client = this.requireClient();
|
const client = this.requireClient();
|
||||||
|
|
||||||
const result = await this.withTimeout(`execute command (${label})`, () =>
|
const result = await this.withTimeout(`execute command (${label})`, () => client.executeCommand(sandboxId, command));
|
||||||
client.executeCommand(sandboxId, command)
|
|
||||||
);
|
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(`daytona ${label} failed (${result.exitCode}): ${result.result}`);
|
throw new Error(`daytona ${label} failed (${result.exitCode}): ${result.result}`);
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +171,7 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
capabilities(): ProviderCapabilities {
|
capabilities(): ProviderCapabilities {
|
||||||
return {
|
return {
|
||||||
remote: true,
|
remote: true,
|
||||||
supportsSessionReuse: true
|
supportsSessionReuse: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +187,7 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
workspaceId: req.workspaceId,
|
workspaceId: req.workspaceId,
|
||||||
repoId: req.repoId,
|
repoId: req.repoId,
|
||||||
handoffId: req.handoffId,
|
handoffId: req.handoffId,
|
||||||
branchName: req.branchName
|
branchName: req.branchName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createStartedAt = Date.now();
|
const createStartedAt = Date.now();
|
||||||
|
|
@ -212,12 +203,12 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
"openhandoff.branch": req.branchName,
|
"openhandoff.branch": req.branchName,
|
||||||
},
|
},
|
||||||
autoStopInterval: this.config.autoStopInterval,
|
autoStopInterval: this.config.autoStopInterval,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
emitDebug("daytona.createSandbox.created", {
|
emitDebug("daytona.createSandbox.created", {
|
||||||
sandboxId: sandbox.id,
|
sandboxId: sandbox.id,
|
||||||
durationMs: Date.now() - createStartedAt,
|
durationMs: Date.now() - createStartedAt,
|
||||||
state: sandbox.state ?? null
|
state: sandbox.state ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const repoDir = `/home/daytona/openhandoff/${req.workspaceId}/${req.repoId}/${req.handoffId}/repo`;
|
const repoDir = `/home/daytona/openhandoff/${req.workspaceId}/${req.repoId}/${req.handoffId}/repo`;
|
||||||
|
|
@ -229,13 +220,13 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
[
|
[
|
||||||
"bash",
|
"bash",
|
||||||
"-lc",
|
"-lc",
|
||||||
`'set -euo pipefail; export DEBIAN_FRONTEND=noninteractive; if command -v git >/dev/null 2>&1 && command -v npx >/dev/null 2>&1; then exit 0; fi; apt-get update -y >/tmp/apt-update.log 2>&1; apt-get install -y git openssh-client ca-certificates nodejs npm >/tmp/apt-install.log 2>&1'`
|
`'set -euo pipefail; export DEBIAN_FRONTEND=noninteractive; if command -v git >/dev/null 2>&1 && command -v npx >/dev/null 2>&1; then exit 0; fi; apt-get update -y >/tmp/apt-update.log 2>&1; apt-get install -y git openssh-client ca-certificates nodejs npm >/tmp/apt-install.log 2>&1'`,
|
||||||
].join(" "),
|
].join(" "),
|
||||||
"install git + node toolchain"
|
"install git + node toolchain",
|
||||||
);
|
);
|
||||||
emitDebug("daytona.createSandbox.install_toolchain.done", {
|
emitDebug("daytona.createSandbox.install_toolchain.done", {
|
||||||
sandboxId: sandbox.id,
|
sandboxId: sandbox.id,
|
||||||
durationMs: Date.now() - installStartedAt
|
durationMs: Date.now() - installStartedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cloneStartedAt = Date.now();
|
const cloneStartedAt = Date.now();
|
||||||
|
|
@ -260,14 +251,14 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
`if git show-ref --verify --quiet "refs/remotes/origin/${req.branchName}"; then git checkout -B "${req.branchName}" "origin/${req.branchName}"; else git checkout -B "${req.branchName}" "$(git branch --show-current 2>/dev/null || echo main)"; fi`,
|
`if git show-ref --verify --quiet "refs/remotes/origin/${req.branchName}"; then git checkout -B "${req.branchName}" "origin/${req.branchName}"; else git checkout -B "${req.branchName}" "$(git branch --show-current 2>/dev/null || echo main)"; fi`,
|
||||||
`git config user.email "openhandoff@local" >/dev/null 2>&1 || true`,
|
`git config user.email "openhandoff@local" >/dev/null 2>&1 || true`,
|
||||||
`git config user.name "OpenHandoff" >/dev/null 2>&1 || true`,
|
`git config user.name "OpenHandoff" >/dev/null 2>&1 || true`,
|
||||||
].join("; ")
|
].join("; "),
|
||||||
)}`
|
)}`,
|
||||||
].join(" "),
|
].join(" "),
|
||||||
"clone repo"
|
"clone repo",
|
||||||
);
|
);
|
||||||
emitDebug("daytona.createSandbox.clone_repo.done", {
|
emitDebug("daytona.createSandbox.clone_repo.done", {
|
||||||
sandboxId: sandbox.id,
|
sandboxId: sandbox.id,
|
||||||
durationMs: Date.now() - cloneStartedAt
|
durationMs: Date.now() - cloneStartedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -280,7 +271,7 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
remote: true,
|
remote: true,
|
||||||
state: sandbox.state ?? null,
|
state: sandbox.state ?? null,
|
||||||
cwd: repoDir,
|
cwd: repoDir,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,17 +281,12 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
await this.ensureStarted(req.sandboxId);
|
await this.ensureStarted(req.sandboxId);
|
||||||
|
|
||||||
// Reconstruct cwd from sandbox labels written at create time.
|
// Reconstruct cwd from sandbox labels written at create time.
|
||||||
const info = await this.withTimeout("resume get sandbox", () =>
|
const info = await this.withTimeout("resume get sandbox", () => client.getSandbox(req.sandboxId));
|
||||||
client.getSandbox(req.sandboxId)
|
|
||||||
);
|
|
||||||
const labels = info.labels ?? {};
|
const labels = info.labels ?? {};
|
||||||
const workspaceId = labels["openhandoff.workspace"] ?? req.workspaceId;
|
const workspaceId = labels["openhandoff.workspace"] ?? req.workspaceId;
|
||||||
const repoId = labels["openhandoff.repo_id"] ?? "";
|
const repoId = labels["openhandoff.repo_id"] ?? "";
|
||||||
const handoffId = labels["openhandoff.handoff"] ?? "";
|
const handoffId = labels["openhandoff.handoff"] ?? "";
|
||||||
const cwd =
|
const cwd = repoId && handoffId ? `/home/daytona/openhandoff/${workspaceId}/${repoId}/${handoffId}/repo` : null;
|
||||||
repoId && handoffId
|
|
||||||
? `/home/daytona/openhandoff/${workspaceId}/${repoId}/${handoffId}/repo`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sandboxId: req.sandboxId,
|
sandboxId: req.sandboxId,
|
||||||
|
|
@ -309,7 +295,7 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
resumed: true,
|
resumed: true,
|
||||||
endpoint: this.config.endpoint ?? null,
|
endpoint: this.config.endpoint ?? null,
|
||||||
...(cwd ? { cwd } : {}),
|
...(cwd ? { cwd } : {}),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -359,9 +345,9 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
[
|
[
|
||||||
"bash",
|
"bash",
|
||||||
"-lc",
|
"-lc",
|
||||||
`'set -euo pipefail; if command -v curl >/dev/null 2>&1; then exit 0; fi; export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/tmp/apt-update.log 2>&1; apt-get install -y curl ca-certificates >/tmp/apt-install.log 2>&1'`
|
`'set -euo pipefail; if command -v curl >/dev/null 2>&1; then exit 0; fi; export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/tmp/apt-update.log 2>&1; apt-get install -y curl ca-certificates >/tmp/apt-install.log 2>&1'`,
|
||||||
].join(" "),
|
].join(" "),
|
||||||
"install curl"
|
"install curl",
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.runCheckedCommand(
|
await this.runCheckedCommand(
|
||||||
|
|
@ -369,9 +355,9 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
[
|
[
|
||||||
"bash",
|
"bash",
|
||||||
"-lc",
|
"-lc",
|
||||||
`'set -euo pipefail; if command -v npx >/dev/null 2>&1; then exit 0; fi; export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/tmp/apt-update.log 2>&1; apt-get install -y nodejs npm >/tmp/apt-install.log 2>&1'`
|
`'set -euo pipefail; if command -v npx >/dev/null 2>&1; then exit 0; fi; export DEBIAN_FRONTEND=noninteractive; apt-get update -y >/tmp/apt-update.log 2>&1; apt-get install -y nodejs npm >/tmp/apt-install.log 2>&1'`,
|
||||||
].join(" "),
|
].join(" "),
|
||||||
"install node toolchain"
|
"install node toolchain",
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.runCheckedCommand(
|
await this.runCheckedCommand(
|
||||||
|
|
@ -379,9 +365,9 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
[
|
[
|
||||||
"bash",
|
"bash",
|
||||||
"-lc",
|
"-lc",
|
||||||
`'set -euo pipefail; export PATH="$HOME/.local/bin:$PATH"; if sandbox-agent --version 2>/dev/null | grep -q "${DaytonaProvider.SANDBOX_AGENT_VERSION}"; then exit 0; fi; curl -fsSL https://releases.rivet.dev/sandbox-agent/${DaytonaProvider.SANDBOX_AGENT_VERSION}/install.sh | sh'`
|
`'set -euo pipefail; export PATH="$HOME/.local/bin:$PATH"; if sandbox-agent --version 2>/dev/null | grep -q "${DaytonaProvider.SANDBOX_AGENT_VERSION}"; then exit 0; fi; curl -fsSL https://releases.rivet.dev/sandbox-agent/${DaytonaProvider.SANDBOX_AGENT_VERSION}/install.sh | sh'`,
|
||||||
].join(" "),
|
].join(" "),
|
||||||
"install sandbox-agent"
|
"install sandbox-agent",
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const agentId of DaytonaProvider.AGENT_IDS) {
|
for (const agentId of DaytonaProvider.AGENT_IDS) {
|
||||||
|
|
@ -389,7 +375,7 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
await this.runCheckedCommand(
|
await this.runCheckedCommand(
|
||||||
req.sandboxId,
|
req.sandboxId,
|
||||||
["bash", "-lc", `'export PATH="$HOME/.local/bin:$PATH"; sandbox-agent install-agent ${agentId}'`].join(" "),
|
["bash", "-lc", `'export PATH="$HOME/.local/bin:$PATH"; sandbox-agent install-agent ${agentId}'`].join(" "),
|
||||||
`install agent ${agentId}`
|
`install agent ${agentId}`,
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// Some sandbox-agent builds may not ship every agent plugin; treat this as best-effort.
|
// Some sandbox-agent builds may not ship every agent plugin; treat this as best-effort.
|
||||||
|
|
@ -401,9 +387,9 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
[
|
[
|
||||||
"bash",
|
"bash",
|
||||||
"-lc",
|
"-lc",
|
||||||
`'set -euo pipefail; export PATH="$HOME/.local/bin:$PATH"; command -v sandbox-agent >/dev/null 2>&1; if pgrep -x sandbox-agent >/dev/null; then exit 0; fi; nohup env SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=${acpRequestTimeoutMs} sandbox-agent server --no-token --host 0.0.0.0 --port ${DaytonaProvider.SANDBOX_AGENT_PORT} >/tmp/sandbox-agent.log 2>&1 &'`
|
`'set -euo pipefail; export PATH="$HOME/.local/bin:$PATH"; command -v sandbox-agent >/dev/null 2>&1; if pgrep -x sandbox-agent >/dev/null; then exit 0; fi; nohup env SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=${acpRequestTimeoutMs} sandbox-agent server --no-token --host 0.0.0.0 --port ${DaytonaProvider.SANDBOX_AGENT_PORT} >/tmp/sandbox-agent.log 2>&1 &'`,
|
||||||
].join(" "),
|
].join(" "),
|
||||||
"start sandbox-agent"
|
"start sandbox-agent",
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.runCheckedCommand(
|
await this.runCheckedCommand(
|
||||||
|
|
@ -411,18 +397,16 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
[
|
[
|
||||||
"bash",
|
"bash",
|
||||||
"-lc",
|
"-lc",
|
||||||
`'for i in $(seq 1 45); do curl -fsS "http://127.0.0.1:${DaytonaProvider.SANDBOX_AGENT_PORT}/v1/health" >/dev/null && exit 0; sleep 1; done; echo "sandbox-agent failed to become healthy" >&2; tail -n 80 /tmp/sandbox-agent.log >&2; exit 1'`
|
`'for i in $(seq 1 45); do curl -fsS "http://127.0.0.1:${DaytonaProvider.SANDBOX_AGENT_PORT}/v1/health" >/dev/null && exit 0; sleep 1; done; echo "sandbox-agent failed to become healthy" >&2; tail -n 80 /tmp/sandbox-agent.log >&2; exit 1'`,
|
||||||
].join(" "),
|
].join(" "),
|
||||||
"wait for sandbox-agent health"
|
"wait for sandbox-agent health",
|
||||||
);
|
);
|
||||||
|
|
||||||
const preview = await this.withTimeout("get preview endpoint", () =>
|
const preview = await this.withTimeout("get preview endpoint", () => client.getPreviewEndpoint(req.sandboxId, DaytonaProvider.SANDBOX_AGENT_PORT));
|
||||||
client.getPreviewEndpoint(req.sandboxId, DaytonaProvider.SANDBOX_AGENT_PORT)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
endpoint: preview.url,
|
endpoint: preview.url,
|
||||||
token: preview.token
|
token: preview.token,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,9 +420,7 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sandbox = await this.withTimeout("health get sandbox", () =>
|
const sandbox = await this.withTimeout("health get sandbox", () => client.getSandbox(req.sandboxId));
|
||||||
client.getSandbox(req.sandboxId)
|
|
||||||
);
|
|
||||||
const state = String(sandbox.state ?? "unknown");
|
const state = String(sandbox.state ?? "unknown");
|
||||||
if (state.toLowerCase().includes("error")) {
|
if (state.toLowerCase().includes("error")) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -461,15 +443,13 @@ export class DaytonaProvider implements SandboxProvider {
|
||||||
|
|
||||||
async attachTarget(req: AttachTargetRequest): Promise<AttachTarget> {
|
async attachTarget(req: AttachTargetRequest): Promise<AttachTarget> {
|
||||||
return {
|
return {
|
||||||
target: `daytona://${req.sandboxId}`
|
target: `daytona://${req.sandboxId}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeCommand(req: ExecuteSandboxCommandRequest): Promise<ExecuteSandboxCommandResult> {
|
async executeCommand(req: ExecuteSandboxCommandRequest): Promise<ExecuteSandboxCommandResult> {
|
||||||
const client = this.requireClient();
|
const client = this.requireClient();
|
||||||
await this.ensureStarted(req.sandboxId);
|
await this.ensureStarted(req.sandboxId);
|
||||||
return await this.withTimeout(`execute command (${req.label ?? "command"})`, () =>
|
return await this.withTimeout(`execute command (${req.label ?? "command"})`, () => client.executeCommand(req.sandboxId, req.command));
|
||||||
client.executeCommand(req.sandboxId, req.command)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,19 +42,25 @@ export function createProviderRegistry(config: AppConfig, driver?: BackendDriver
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const local = new LocalProvider({
|
const local = new LocalProvider(
|
||||||
rootDir: config.providers.local.rootDir,
|
{
|
||||||
sandboxAgentPort: config.providers.local.sandboxAgentPort,
|
rootDir: config.providers.local.rootDir,
|
||||||
}, gitDriver);
|
sandboxAgentPort: config.providers.local.sandboxAgentPort,
|
||||||
const daytona = new DaytonaProvider({
|
},
|
||||||
endpoint: config.providers.daytona.endpoint,
|
gitDriver,
|
||||||
apiKey: config.providers.daytona.apiKey,
|
);
|
||||||
image: config.providers.daytona.image
|
const daytona = new DaytonaProvider(
|
||||||
}, driver?.daytona);
|
{
|
||||||
|
endpoint: config.providers.daytona.endpoint,
|
||||||
|
apiKey: config.providers.daytona.apiKey,
|
||||||
|
image: config.providers.daytona.image,
|
||||||
|
},
|
||||||
|
driver?.daytona,
|
||||||
|
);
|
||||||
|
|
||||||
const map: Record<ProviderId, SandboxProvider> = {
|
const map: Record<ProviderId, SandboxProvider> = {
|
||||||
local,
|
local,
|
||||||
daytona
|
daytona,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -66,6 +72,6 @@ export function createProviderRegistry(config: AppConfig, driver?: BackendDriver
|
||||||
},
|
},
|
||||||
defaultProviderId(): ProviderId {
|
defaultProviderId(): ProviderId {
|
||||||
return config.providers.daytona.apiKey ? "daytona" : "local";
|
return config.providers.daytona.apiKey ? "daytona" : "local";
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,7 @@ function expandHome(value: string): string {
|
||||||
|
|
||||||
async function branchExists(repoPath: string, branchName: string): Promise<boolean> {
|
async function branchExists(repoPath: string, branchName: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await execFileAsync("git", [
|
await execFileAsync("git", ["-C", repoPath, "show-ref", "--verify", `refs/remotes/origin/${branchName}`]);
|
||||||
"-C",
|
|
||||||
repoPath,
|
|
||||||
"show-ref",
|
|
||||||
"--verify",
|
|
||||||
`refs/remotes/origin/${branchName}`,
|
|
||||||
]);
|
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -59,9 +53,7 @@ async function branchExists(repoPath: string, branchName: string): Promise<boole
|
||||||
|
|
||||||
async function checkoutBranch(repoPath: string, branchName: string, git: GitDriver): Promise<void> {
|
async function checkoutBranch(repoPath: string, branchName: string, git: GitDriver): Promise<void> {
|
||||||
await git.fetch(repoPath);
|
await git.fetch(repoPath);
|
||||||
const targetRef = (await branchExists(repoPath, branchName))
|
const targetRef = (await branchExists(repoPath, branchName)) ? `origin/${branchName}` : await git.remoteDefaultBaseRef(repoPath);
|
||||||
? `origin/${branchName}`
|
|
||||||
: await git.remoteDefaultBaseRef(repoPath);
|
|
||||||
await execFileAsync("git", ["-C", repoPath, "checkout", "-B", branchName, targetRef], {
|
await execFileAsync("git", ["-C", repoPath, "checkout", "-B", branchName, targetRef], {
|
||||||
env: process.env as Record<string, string>,
|
env: process.env as Record<string, string>,
|
||||||
});
|
});
|
||||||
|
|
@ -76,9 +68,7 @@ export class LocalProvider implements SandboxProvider {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private rootDir(): string {
|
private rootDir(): string {
|
||||||
return expandHome(
|
return expandHome(this.config.rootDir?.trim() || "~/.local/share/openhandoff/local-sandboxes");
|
||||||
this.config.rootDir?.trim() || "~/.local/share/openhandoff/local-sandboxes",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sandboxRoot(workspaceId: string, sandboxId: string): string {
|
private sandboxRoot(workspaceId: string, sandboxId: string): string {
|
||||||
|
|
@ -89,11 +79,7 @@ export class LocalProvider implements SandboxProvider {
|
||||||
return resolve(this.sandboxRoot(workspaceId, sandboxId), "repo");
|
return resolve(this.sandboxRoot(workspaceId, sandboxId), "repo");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sandboxHandle(
|
private sandboxHandle(workspaceId: string, sandboxId: string, repoDir: string): SandboxHandle {
|
||||||
workspaceId: string,
|
|
||||||
sandboxId: string,
|
|
||||||
repoDir: string,
|
|
||||||
): SandboxHandle {
|
|
||||||
return {
|
return {
|
||||||
sandboxId,
|
sandboxId,
|
||||||
switchTarget: `local://${repoDir}`,
|
switchTarget: `local://${repoDir}`,
|
||||||
|
|
@ -242,9 +228,7 @@ export class LocalProvider implements SandboxProvider {
|
||||||
const detail = error as { stdout?: string; stderr?: string; code?: number };
|
const detail = error as { stdout?: string; stderr?: string; code?: number };
|
||||||
return {
|
return {
|
||||||
exitCode: typeof detail.code === "number" ? detail.code : 1,
|
exitCode: typeof detail.code === "number" ? detail.code : 1,
|
||||||
result: [detail.stdout, detail.stderr, error instanceof Error ? error.message : String(error)]
|
result: [detail.stdout, detail.stderr, error instanceof Error ? error.message : String(error)].filter(Boolean).join(""),
|
||||||
.filter(Boolean)
|
|
||||||
.join(""),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,14 @@ export function deriveFallbackTitle(task: string, explicitTitle?: string): strin
|
||||||
|
|
||||||
const lowered = source.toLowerCase();
|
const lowered = source.toLowerCase();
|
||||||
|
|
||||||
const typePrefix = lowered.includes("fix") || lowered.includes("bug")
|
const typePrefix =
|
||||||
? "fix"
|
lowered.includes("fix") || lowered.includes("bug")
|
||||||
: lowered.includes("doc") || lowered.includes("readme")
|
? "fix"
|
||||||
? "docs"
|
: lowered.includes("doc") || lowered.includes("readme")
|
||||||
: lowered.includes("refactor")
|
? "docs"
|
||||||
? "refactor"
|
: lowered.includes("refactor")
|
||||||
: "feat";
|
? "refactor"
|
||||||
|
: "feat";
|
||||||
|
|
||||||
const cleaned = source
|
const cleaned = source
|
||||||
.split("")
|
.split("")
|
||||||
|
|
@ -88,9 +89,7 @@ export function sanitizeBranchName(input: string): string {
|
||||||
return trimmed.slice(0, 50).replace(/-+$/g, "");
|
return trimmed.slice(0, 50).replace(/-+$/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveCreateFlowDecision(
|
export function resolveCreateFlowDecision(input: ResolveCreateFlowDecisionInput): ResolveCreateFlowDecisionResult {
|
||||||
input: ResolveCreateFlowDecisionInput
|
|
||||||
): ResolveCreateFlowDecisionResult {
|
|
||||||
const explicitBranch = input.explicitBranchName?.trim();
|
const explicitBranch = input.explicitBranchName?.trim();
|
||||||
const title = deriveFallbackTitle(input.task, input.explicitTitle);
|
const title = deriveFallbackTitle(input.task, input.explicitTitle);
|
||||||
const generatedBase = sanitizeBranchName(title) || "handoff";
|
const generatedBase = sanitizeBranchName(title) || "handoff";
|
||||||
|
|
@ -98,16 +97,11 @@ export function resolveCreateFlowDecision(
|
||||||
const branchBase = explicitBranch && explicitBranch.length > 0 ? explicitBranch : generatedBase;
|
const branchBase = explicitBranch && explicitBranch.length > 0 ? explicitBranch : generatedBase;
|
||||||
|
|
||||||
const existingBranches = new Set(input.localBranches.map((value) => value.trim()).filter((value) => value.length > 0));
|
const existingBranches = new Set(input.localBranches.map((value) => value.trim()).filter((value) => value.length > 0));
|
||||||
const existingHandoffBranches = new Set(
|
const existingHandoffBranches = new Set(input.handoffBranches.map((value) => value.trim()).filter((value) => value.length > 0));
|
||||||
input.handoffBranches.map((value) => value.trim()).filter((value) => value.length > 0)
|
const conflicts = (name: string): boolean => existingBranches.has(name) || existingHandoffBranches.has(name);
|
||||||
);
|
|
||||||
const conflicts = (name: string): boolean =>
|
|
||||||
existingBranches.has(name) || existingHandoffBranches.has(name);
|
|
||||||
|
|
||||||
if (explicitBranch && conflicts(branchBase)) {
|
if (explicitBranch && conflicts(branchBase)) {
|
||||||
throw new Error(
|
throw new Error(`Branch '${branchBase}' already exists. Choose a different --name/--branch value.`);
|
||||||
`Branch '${branchBase}' already exists. Choose a different --name/--branch value.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (explicitBranch) {
|
if (explicitBranch) {
|
||||||
|
|
@ -123,6 +117,6 @@ export function resolveCreateFlowDecision(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
branchName: candidate
|
branchName: candidate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,6 @@ export function openhandoffDataDir(config: AppConfig): string {
|
||||||
return resolve(dirname(dbPath));
|
return resolve(dirname(dbPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openhandoffRepoClonePath(
|
export function openhandoffRepoClonePath(config: AppConfig, workspaceId: string, repoId: string): string {
|
||||||
config: AppConfig,
|
|
||||||
workspaceId: string,
|
|
||||||
repoId: string
|
|
||||||
): string {
|
|
||||||
return resolve(join(openhandoffDataDir(config), "repos", workspaceId, repoId));
|
return resolve(join(openhandoffDataDir(config), "repos", workspaceId, repoId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,10 @@ export function setWindowStatus(branchName: string, status: string): number {
|
||||||
|
|
||||||
let stdout: string;
|
let stdout: string;
|
||||||
try {
|
try {
|
||||||
stdout = execFileSync(
|
stdout = execFileSync("tmux", ["list-windows", "-a", "-F", "#{session_name}:#{window_id}:#{window_name}"], {
|
||||||
"tmux",
|
encoding: "utf8",
|
||||||
["list-windows", "-a", "-F", "#{session_name}:#{window_id}:#{window_name}"],
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
});
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +50,7 @@ export function setWindowStatus(branchName: string, status: string): number {
|
||||||
|
|
||||||
const newName = `${symbol} ${branchName}`;
|
const newName = `${symbol} ${branchName}`;
|
||||||
spawnSync("tmux", ["rename-window", "-t", `${sessionName}:${windowId}`, newName], {
|
spawnSync("tmux", ["rename-window", "-t", `${sessionName}:${windowId}`, newName], {
|
||||||
stdio: "ignore"
|
stdio: "ignore",
|
||||||
});
|
});
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import { deriveFallbackTitle, resolveCreateFlowDecision, sanitizeBranchName } from "../src/services/create-flow.js";
|
||||||
deriveFallbackTitle,
|
|
||||||
resolveCreateFlowDecision,
|
|
||||||
sanitizeBranchName
|
|
||||||
} from "../src/services/create-flow.js";
|
|
||||||
|
|
||||||
describe("create flow decision", () => {
|
describe("create flow decision", () => {
|
||||||
it("derives a conventional-style fallback title from task text", () => {
|
it("derives a conventional-style fallback title from task text", () => {
|
||||||
|
|
@ -25,7 +21,7 @@ describe("create flow decision", () => {
|
||||||
const resolved = resolveCreateFlowDecision({
|
const resolved = resolveCreateFlowDecision({
|
||||||
task: "Add auth",
|
task: "Add auth",
|
||||||
localBranches: ["feat-add-auth"],
|
localBranches: ["feat-add-auth"],
|
||||||
handoffBranches: ["feat-add-auth-2"]
|
handoffBranches: ["feat-add-auth-2"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(resolved.title).toBe("feat: Add auth");
|
expect(resolved.title).toBe("feat: Add auth");
|
||||||
|
|
@ -38,8 +34,8 @@ describe("create flow decision", () => {
|
||||||
task: "new task",
|
task: "new task",
|
||||||
explicitBranchName: "existing-branch",
|
explicitBranchName: "existing-branch",
|
||||||
localBranches: ["existing-branch"],
|
localBranches: ["existing-branch"],
|
||||||
handoffBranches: []
|
handoffBranches: [],
|
||||||
})
|
}),
|
||||||
).toThrow("already exists");
|
).toThrow("already exists");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ function createProviderWithClient(client: DaytonaClientLike): DaytonaProvider {
|
||||||
apiKey: "test-key",
|
apiKey: "test-key",
|
||||||
image: "ubuntu:24.04",
|
image: "ubuntu:24.04",
|
||||||
},
|
},
|
||||||
daytonaDriver
|
daytonaDriver,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ describe("daytona provider snapshot image behavior", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const startCommand = client.executedCommands.find((command) =>
|
const startCommand = client.executedCommands.find((command) =>
|
||||||
command.includes("nohup env SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=240000 sandbox-agent server")
|
command.includes("nohup env SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=240000 sandbox-agent server"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const joined = client.executedCommands.join("\n");
|
const joined = client.executedCommands.join("\n");
|
||||||
|
|
@ -149,13 +149,15 @@ describe("daytona provider snapshot image behavior", () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const provider = createProviderWithClient(hangingClient);
|
const provider = createProviderWithClient(hangingClient);
|
||||||
await expect(provider.createSandbox({
|
await expect(
|
||||||
workspaceId: "default",
|
provider.createSandbox({
|
||||||
repoId: "repo-1",
|
workspaceId: "default",
|
||||||
repoRemote: "https://github.com/acme/repo.git",
|
repoId: "repo-1",
|
||||||
branchName: "feature/test",
|
repoRemote: "https://github.com/acme/repo.git",
|
||||||
handoffId: "handoff-timeout",
|
branchName: "feature/test",
|
||||||
})).rejects.toThrow("daytona create sandbox timed out after 120ms");
|
handoffId: "handoff-timeout",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("daytona create sandbox timed out after 120ms");
|
||||||
} finally {
|
} finally {
|
||||||
if (previous === undefined) {
|
if (previous === undefined) {
|
||||||
delete process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS;
|
delete process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS;
|
||||||
|
|
@ -173,7 +175,7 @@ describe("daytona provider snapshot image behavior", () => {
|
||||||
workspaceId: "default",
|
workspaceId: "default",
|
||||||
sandboxId: "sandbox-1",
|
sandboxId: "sandbox-1",
|
||||||
command: "echo backend-push",
|
command: "echo backend-push",
|
||||||
label: "manual push"
|
label: "manual push",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue