mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-04 00:23:53 +08:00
* fix(examples): harden multi-agent notepad 409 retry and improve docs Add decorrelated jitter to the GitHub Contents API retry loop so racing workers don't retry in lockstep, and widen the retry set to 5xx for transient server errors. Without jitter, N concurrent workers trip the same backoff schedule and re-collide on each beat. Also: - Extract the inline 220-line sandbox runner from demo.sh into a separate runner.sh. demo.sh stays as host orchestration; runner.sh is the per-sandbox program. demo.sh shrinks 460 to 240 lines. - Rewrite README to lead with "Why GitHub as a shared notepad?" and add a "Memory architecture variants" section covering pile, append-journal, and indexed-memory patterns. - Switch Quick Start to use \`gh auth token\` as the primary path. - Pass the worker slice arg explicitly instead of reading \$9 from the outer dispatcher. - Add .gitignore for personal run_demo.sh helpers. * docs updates
236 lines
7.1 KiB
Bash
Executable File
236 lines
7.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# This script runs INSIDE each OpenShell sandbox.
|
|
#
|
|
# It is uploaded to /sandbox/payload/demo-runner.sh by demo.sh and invoked as
|
|
# the sandbox entrypoint. It receives positional arguments from `run_sandbox()`
|
|
# in demo.sh and reads DEMO_GITHUB_TOKEN from the environment, where the
|
|
# OpenShell proxy resolves the provider placeholder at the network boundary.
|
|
#
|
|
# Modes:
|
|
# worker — render the worker prompt for a slice, run codex, PUT the note.
|
|
# synthesis — read every worker note, run the synthesis prompt, PUT summary.
|
|
|
|
set -euo pipefail
|
|
|
|
MODE="$1"
|
|
OWNER="$2"
|
|
REPO="$3"
|
|
BRANCH="$4"
|
|
RUN_ID="$5"
|
|
AGENT_INDEX="${6:-0}"
|
|
AGENT_COUNT="${7:-0}"
|
|
TOPIC="${8:-}"
|
|
|
|
api_url() {
|
|
printf 'https://api.github.com%s' "$1"
|
|
}
|
|
|
|
github_request() {
|
|
local method="$1"
|
|
local path="$2"
|
|
local output="$3"
|
|
shift 3
|
|
curl -sS \
|
|
-X "$method" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
-H "Authorization: Bearer ${DEMO_GITHUB_TOKEN}" \
|
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
"$@" \
|
|
-o "$output" \
|
|
-w "%{http_code}" \
|
|
"$(api_url "$path")"
|
|
}
|
|
|
|
render_template() {
|
|
local template="$1"
|
|
local slice="${2:-}"
|
|
node - "$template" "$AGENT_INDEX" "$AGENT_COUNT" "$TOPIC" "$slice" <<'NODE'
|
|
const fs = require("fs");
|
|
const [templatePath, agentIndex, agentCount, topic, slice] = process.argv.slice(2);
|
|
let text = fs.readFileSync(templatePath, "utf8");
|
|
text = text.replace(/^(?:<!-- [^\n]* -->\n)+\n?/, "");
|
|
const replacements = {
|
|
"{{AGENT_INDEX}}": agentIndex,
|
|
"{{AGENT_COUNT}}": agentCount,
|
|
"{{TOPIC}}": topic,
|
|
"{{SLICE}}": slice,
|
|
};
|
|
for (const [needle, value] of Object.entries(replacements)) {
|
|
text = text.split(needle).join(value);
|
|
}
|
|
process.stdout.write(text);
|
|
NODE
|
|
}
|
|
|
|
bootstrap_codex_oauth() {
|
|
mkdir -p "$HOME/.codex"
|
|
node - <<'NODE'
|
|
const fs = require("fs");
|
|
const path = `${process.env.HOME}/.codex/auth.json`;
|
|
const b64u = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const fakeIdToken = [
|
|
b64u({ alg: "none", typ: "JWT" }),
|
|
b64u({
|
|
iss: "https://auth.openai.com",
|
|
aud: "codex",
|
|
sub: "openshell-placeholder",
|
|
email: "placeholder@example.com",
|
|
iat: now,
|
|
exp: now + 3600,
|
|
}),
|
|
"placeholder",
|
|
].join(".");
|
|
|
|
fs.writeFileSync(path, JSON.stringify({
|
|
auth_mode: "chatgpt",
|
|
OPENAI_API_KEY: null,
|
|
tokens: {
|
|
id_token: fakeIdToken,
|
|
access_token: process.env.CODEX_AUTH_ACCESS_TOKEN,
|
|
refresh_token: process.env.CODEX_AUTH_REFRESH_TOKEN,
|
|
account_id: process.env.CODEX_AUTH_ACCOUNT_ID,
|
|
},
|
|
last_refresh: new Date().toISOString(),
|
|
}, null, 2));
|
|
NODE
|
|
chmod 600 "$HOME/.codex/auth.json"
|
|
}
|
|
|
|
put_contents() {
|
|
local repo_path="$1"
|
|
local source_file="$2"
|
|
local message="$3"
|
|
local get_body put_body put_response status sha
|
|
local attempt=0
|
|
local max_attempts=6
|
|
local sleep_secs=1
|
|
get_body="$(mktemp)"
|
|
put_body="$(mktemp)"
|
|
put_response="$(mktemp)"
|
|
|
|
while (( attempt < max_attempts )); do
|
|
attempt=$((attempt + 1))
|
|
|
|
status="$(github_request GET "/repos/${OWNER}/${REPO}/contents/${repo_path}?ref=${BRANCH}" "$get_body")"
|
|
if [[ "$status" == "200" ]]; then
|
|
sha="$(node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(p.sha || "");' "$get_body")"
|
|
elif [[ "$status" == "404" ]]; then
|
|
sha=""
|
|
else
|
|
echo "GitHub GET ${repo_path} failed with HTTP ${status}" >&2
|
|
cat "$get_body" >&2
|
|
return 1
|
|
fi
|
|
|
|
node - "$source_file" "$message" "$BRANCH" "$sha" > "$put_body" <<'NODE'
|
|
const fs = require("fs");
|
|
const [file, message, branch, sha] = process.argv.slice(2);
|
|
const body = {
|
|
message,
|
|
branch,
|
|
content: fs.readFileSync(file).toString("base64"),
|
|
};
|
|
if (sha) body.sha = sha;
|
|
process.stdout.write(JSON.stringify(body));
|
|
NODE
|
|
|
|
status="$(github_request PUT "/repos/${OWNER}/${REPO}/contents/${repo_path}" "$put_response" --data-binary "@${put_body}")"
|
|
if [[ "$status" == "200" || "$status" == "201" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Retry on 409 (concurrent writer) and 5xx (transient server error)
|
|
# with decorrelated jitter so racing workers don't retry in lockstep.
|
|
if [[ "$status" =~ ^(409|5[0-9][0-9])$ && $attempt -lt $max_attempts ]]; then
|
|
sleep "$(( sleep_secs + RANDOM % sleep_secs ))"
|
|
sleep_secs=$((sleep_secs * 2))
|
|
continue
|
|
fi
|
|
|
|
echo "GitHub PUT ${repo_path} failed with HTTP ${status} (attempt ${attempt}/${max_attempts})" >&2
|
|
cat "$put_response" >&2
|
|
return 1
|
|
done
|
|
}
|
|
|
|
get_contents_file() {
|
|
local repo_path="$1"
|
|
local destination="$2"
|
|
local body status
|
|
body="$(mktemp)"
|
|
status="$(github_request GET "/repos/${OWNER}/${REPO}/contents/${repo_path}?ref=${BRANCH}" "$body")"
|
|
if [[ "$status" != "200" ]]; then
|
|
echo "GitHub GET ${repo_path} failed with HTTP ${status}" >&2
|
|
cat "$body" >&2
|
|
return 1
|
|
fi
|
|
node - "$body" "$destination" <<'NODE'
|
|
const fs = require("fs");
|
|
const [bodyPath, destination] = process.argv.slice(2);
|
|
const body = JSON.parse(fs.readFileSync(bodyPath, "utf8"));
|
|
fs.writeFileSync(destination, Buffer.from((body.content || "").replace(/\s/g, ""), "base64"));
|
|
NODE
|
|
}
|
|
|
|
run_codex_to_file() {
|
|
local prompt_file="$1"
|
|
local output_file="$2"
|
|
codex exec \
|
|
--skip-git-repo-check \
|
|
--sandbox read-only \
|
|
--ephemeral \
|
|
--output-last-message "$output_file" \
|
|
"$(cat "$prompt_file")"
|
|
}
|
|
|
|
worker() {
|
|
local slice="$1"
|
|
local prompt_file output_file repo_path
|
|
bootstrap_codex_oauth
|
|
prompt_file="$(mktemp)"
|
|
output_file="$(mktemp)"
|
|
repo_path="runs/${RUN_ID}/notes/agent-${AGENT_INDEX}.md"
|
|
|
|
render_template /sandbox/payload/prompts/worker.md "$slice" > "$prompt_file"
|
|
run_codex_to_file "$prompt_file" "$output_file"
|
|
put_contents "$repo_path" "$output_file" "Add agent ${AGENT_INDEX} note for ${RUN_ID}"
|
|
printf 'wrote %s\n' "$repo_path"
|
|
}
|
|
|
|
synthesis() {
|
|
local notes_dir prompt_file output_file repo_path
|
|
bootstrap_codex_oauth
|
|
notes_dir="$(mktemp -d)"
|
|
prompt_file="$(mktemp)"
|
|
output_file="$(mktemp)"
|
|
repo_path="runs/${RUN_ID}/summary.md"
|
|
|
|
for i in $(seq 1 "$AGENT_COUNT"); do
|
|
get_contents_file "runs/${RUN_ID}/notes/agent-${i}.md" "${notes_dir}/agent-${i}.md"
|
|
done
|
|
|
|
render_template /sandbox/payload/prompts/synthesis.md "" > "$prompt_file"
|
|
{
|
|
printf '\n\n## Worker Notes\n\n'
|
|
for i in $(seq 1 "$AGENT_COUNT"); do
|
|
printf '\n\n---\n\n'
|
|
cat "${notes_dir}/agent-${i}.md"
|
|
done
|
|
} >> "$prompt_file"
|
|
|
|
run_codex_to_file "$prompt_file" "$output_file"
|
|
put_contents "$repo_path" "$output_file" "Add multi-agent summary for ${RUN_ID}"
|
|
printf 'wrote %s\n' "$repo_path"
|
|
}
|
|
|
|
case "$MODE" in
|
|
worker) worker "${9:-}" ;;
|
|
synthesis) synthesis ;;
|
|
*) echo "unknown mode: $MODE" >&2; exit 2 ;;
|
|
esac
|