Skip to content

feat(agent): Agent-SDK session core with live UI + full runtime (claude_sdk) - #1893

Closed
jsun-m wants to merge 8 commits into
sonichi:mainfrom
jsun-m:feat/agent-claude-sdk
Closed

jsun-m wants to merge 8 commits into
sonichi:mainfrom
jsun-m:feat/agent-claude-sdk

Conversation

@jsun-m

@jsun-m jsun-m commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #1891 + #1892. Until those merge, their commits appear in this diff too — review the top commit here. Rebased onto main after they land.

What

Adds a second core implementation — core_config.core_type: claude_sdk — built on @anthropic-ai/claude-agent-sdk: a persistent, fully-observable Claude Code session behind a live browser UI, carrying the full Sutando runtime (tasks, crons, proactive loop, heartbeat).

The session

sdk/session-server.ts runs one long-lived query() with an async-iterable prompt (persistent multi-turn — the stable SDK form of the CLI's --input/--output-format stream-json). It loads CLAUDE.md + skills (settingSources + the claude_code systemPrompt preset — without these the SDK runs in isolation mode with no runtime). Every SDK message streams to the browser over SSE; POST /input feeds a turn. It replays event history to each new SSE client, so a refresh shows the whole session. ui.html is a minimal single-file client.

Hybrid runtime (host + agent)

CronCreate / Monitor are not usable in an SDK session (verified live), so scheduling/watching live in the Node host:

  • Task ingestion — watches <workspace>/tasks/; on pickup a task is atomically moved to an in-flight staging dir before injection (so the proactive loop's own tasks/ scan can't double-process it), then archived to tasks/archive/YYYY-MM/ on result; staged leftovers recovered on restart.
  • SUTANDO_HOST_OWNS_WATCHER=1 — watch-tasks-stream.sh gains a no-op guard so the agent's own Monitor watcher holds its PID but emits nothing (only the host injects).
  • Cron/proactive scheduling — reads the same hosts/<host>/crons.json, matches each 5-field schedule, and injects the cron's prompt (/proactive-loop on */5, etc.).
  • Liveness — per-host state/cores/<host>.alive heartbeat + state/core-status.json.

Wiring

  • src/startup.sh routes the core by core_config.core_type: claude_cli → the tmux core (unchanged default); claude_sdk → session-server.sh backgrounded (heartbeat deferred to core_heartbeat.py), UI opened.
  • restart.sh / stop.sh now stop the SDK core too.

Notes / status

  • Auto-approves tools (permissionMode: 'bypassPermissions') — a canUseTool UI-approval flow is the natural follow-up.
  • Bound to loopback only (no auth; relays full prompts + tool I/O).
  • Verified: FAKE-mode host runtime (heartbeat, task lifecycle, cron scheduling), the SDK session spawning + emitting events, and event replay across reconnect.

🤖 Generated with Claude Code

…de/cli/

Extract the two scripts that initialize + onboard the Claude Code core agent out
of scripts/ into a dedicated src/agent/claude/cli/, so the launch/onboarding
logic lives in one place (and a future headless / alternate-provider launcher can
grow alongside it):

  scripts/start-cli.sh           -> src/agent/claude/cli/start-cli.sh
  scripts/sutando-shell-setup.sh -> src/agent/claude/cli/sutando-shell-setup.sh

Re-points every caller -- startup.sh, health-check.py (_default_core_restart),
Sutando.app (main.swift), sutando-migrate.sh, the health-check-fallback launchd
plist, docs/host-cli-bindings.md, and comment refs in sutando_config.py/ts +
util_paths.ts -- and fixes each script's repo-root self-location (dirname/.. ->
dirname/../../../.. now that they are four levels deep). Updates the affected
tests and adds src/agent/claude/README.md.

Pure move + reference update; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jsun-m
jsun-m force-pushed the feat/agent-claude-sdk branch from ed79a7c to 939b93d Compare July 1, 2026 20:36
@jsun-m
jsun-m force-pushed the feat/agent-claude-sdk branch from 939b93d to 5b0f0c3 Compare July 1, 2026 22:53
jsun-m and others added 4 commits July 1, 2026 15:57
…er paths

Moving start-cli.sh / sutando-shell-setup.sh into src/agent/claude/cli/ removed
the old scripts/ paths outright. Callers in the repo were repointed, but a
not-yet-rebuilt Sutando.app binary has repoRoot + "/scripts/start-cli.sh"
compiled in and would hard-fail after a `git pull` until rebuilt (same for an
in-flight health-check --recover-core). Add thin exec-forwarding shims at the
old paths for one release so those lagging callers keep working; remove after
all callers reference the new path. New tests/start-cli-compat-shim.test.sh
asserts the shims exist, are executable, parse, and forward — without launching
the live core.

Also fix two stale comments left by the move: the config-dir test said "three
levels up" (now four: cli → claude → agent → src → repo) and the migrate-import
test referenced the pre-move scripts/sutando-shell-setup.sh path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onfig)

Add a high-level `core_config` block that lets the core agent run against a
custom Anthropic-compatible provider + API token instead of the Claude.ai
subscription, plus per-class model overrides for subtasks. Builds on the
src/agent/claude/ restructure (sonichi#1891).

- core_config: { core_type, provider, auth_env, model, models{opus,sonnet,haiku} }
  resolved by src/sutando_config.py::resolve_core_config + the sutando-config.sh
  `core-config` getter.
- provider-env.sh (shared helper): resolves the provider endpoint + token
  (env or vault) + the model aliases (ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL).
- cli/start-cli.sh: when core_config.provider is set, boots the SAME interactive
  core against the provider -- sheds the inherited credential-proxy base URL,
  env token overrides OAuth, refuses if a provider is set but no token resolves.
- cli/dashp.sh: headless one-shot `claude -p` against the provider.
- auth_env defaults to the ANTHROPIC_SUBSCRIPTION sentinel (subscription OAuth,
  no token); a real token env (ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY) selects
  Bearer vs x-api-key, with the other cleared to avoid a mixed-header conflict.

Stacked on sonichi#1891 -- review after it merges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When core_config.auth_env is the default sentinel ANTHROPIC_SUBSCRIPTION, a
shell-set ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN (e.g. exported in ~/.zshrc)
silently overrode the Claude.ai subscription OAuth — Claude Code precedence is
env token > keychain/.credentials.json — so the core billed the raw API (or
401'd on an invalid key) instead of using the subscription the operator asked
for. Clear both stray token vars at launch so auth_env=ANTHROPIC_SUBSCRIPTION
means "subscription, period" rather than trusting a clean env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…precedence

Addresses review findings on the alternate-provider path (provider-env.sh):

- auth_env injection: replaced `eval "have=\${$auth_env}"` with bash indirect
  expansion `${!auth_env}` and added an identifier check (^[A-Za-z_][A-Za-z0-9_]*)
  before it's used for expansion/export. A hostile auth_env from a synced config
  can no longer be executed as code.
- credential leak over the endpoint: when a provider TOKEN will be sent (not the
  ANTHROPIC_SUBSCRIPTION sentinel), refuse a non-https URL to any non-loopback
  host (the token would go out in plaintext) and refuse embedded userinfo
  (user:pass@host leaks via logs/proxies). Loopback http stays allowed — that's
  how the subscription credential-proxy is addressed.
- precedence: an explicit SUTANDO_PROVIDER_URL/_MODEL (the channel dashp.sh's
  flags inject through) now WINS over an inherited ANTHROPIC_BASE_URL/_MODEL,
  restoring the documented CLI > env > config order. Previously an inherited
  ANTHROPIC_MODEL silently no-op'd `dashp.sh --model`.
- config read is LOUD: a malformed sutando.config makes the core-config getter
  exit nonzero; both provider-env.sh and start-cli.sh now capture the rc and warn
  instead of `2>/dev/null … || true`-swallowing it into a silent subscription
  fallback.

New tests/provider-env-guards.test.sh covers all four (injection rejected, http
+ userinfo refused, loopback allowed, override precedence). Existing
start-cli-provider-as-core.test.sh still passes 6/6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jsun-m
jsun-m force-pushed the feat/agent-claude-sdk branch from 5b0f0c3 to 4013fad Compare July 1, 2026 22:58

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inherits #1892's now-hardened provider-env (good — the auth_env whitelist, https/userinfo enforcement, and loud config read all carry).

One design observation to confirm is intentional (not necessarily a fix): all inputs (tasks, crons, user turns, bootstrap) funnel into a single serialized InputStream → one query(), so a long-running task blocks every queued cron/task behind it — no concurrency or preemption. If that's the intended host-driven model for v1, that's a reasonable call; worth a short code comment noting the serialization is deliberate so a future reader doesn't "fix" it into a race.

Holding my approval on this one. At +3063/-1190 it's the largest of the stack, and the automated deep-read passes didn't complete it (timed out on size). Rather than rubber-stamp a 3k-line diff, I'd recommend a focused per-file pass before merge — happy to do that if useful. I don't see anything blocking; I just won't approve a diff this size unread.

import { query } from '@anthropic-ai/claude-agent-sdk';

const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = process.cwd(); // session-server.sh cd's to the repo before exec

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committable fix for the cwd-lint red (the sole failing check) — no behavior change. Reuses HERE (line 37) and the already-imported join (line 34):

Suggested change
const REPO = process.cwd(); // session-server.sh cd's to the repo before exec
const REPO = join(HERE, '..', '..', '..', '..'); // repo root, file-relative (this file is 4 dirs deep)

Note: the lint's auto-hint to use resolveWorkspace() would break this — that's the local function at line 60 returning the workspace (<repo>/workspace/), whereas REPO is the repo root. This file lives at src/agent/claude/sdk/, so 4×.. from HERE resolves to the repo root — identical to the old process.cwd() (since session-server.sh cd's to the repo before exec), but without the cwd dependency the lint guards against.

jsun-m and others added 2 commits July 2, 2026 10:10
…H_TOKEN

Review finding: the subscription-mode cleanup unconditionally unset
ANTHROPIC_AUTH_TOKEN too, which breaks env-token subscription/OAuth setups where
ANTHROPIC_AUTH_TOKEN is the only credential available before .credentials.json
exists. Only ANTHROPIC_API_KEY (x-api-key) is unambiguously "not the
subscription" and is the header the credential-proxy does NOT rewrite — so it's
the one that silently bills the raw API. ANTHROPIC_AUTH_TOKEN can be a valid
subscription/OAuth bearer, and the proxy rewrites the Authorization header
anyway, so leave it untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…de_sdk)

Add an Agent-SDK-based core (core_config.core_type: claude_sdk) that runs a
persistent, observable Claude Code session behind a live browser UI, and carries
the full Sutando runtime. Builds on sonichi#1891 + sonichi#1892.

- sdk/session-server.ts: one long-lived @anthropic-ai/claude-agent-sdk query()
  with async-iterable input (persistent multi-turn) -> SSE event stream + minimal
  ui.html + POST /input. Loads CLAUDE.md + skills (settingSources + claude_code
  systemPrompt preset) so it is not a bare Q&A session; replays event history to
  each new SSE client so a browser refresh shows the full session.
- Hybrid runtime: the Node host owns task ingestion (watch tasks/, stage on
  pickup, inject, archive on result), the per-host liveness heartbeat, and
  cron/proactive scheduling (reads crons.json, injects each prompt at its time --
  CronCreate is not available in an SDK session). SUTANDO_HOST_OWNS_WATCHER=1
  no-ops the agent's Monitor watcher so tasks do not double-process.
- src/startup.sh routes the core by core_config.core_type (claude_cli -> tmux
  core; claude_sdk -> session-server, backgrounded, heartbeat deferred to
  core_heartbeat.py). restart.sh/stop.sh now stop the session-server, and
  watch-tasks-stream.sh gains the host-owns-watcher no-op guard.

Stacked on sonichi#1891 + sonichi#1892 -- review after they merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jsun-m
jsun-m force-pushed the feat/agent-claude-sdk branch from 4013fad to 0395101 Compare July 2, 2026 17:10
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@liususan091219 liususan091219 added the request-change Has an active CHANGES_REQUESTED review label Jul 3, 2026
@liususan091219

Copy link
Copy Markdown
Collaborator

Codex triage findings (detail for the REQUEST_CHANGES review that follows):

Source-grep tests at start-cli-compat-shim.test.sh:21 and start-cli-provider-as-core.test.sh:174 (auto-fail) - rewrite as behavior. Feature itself is large (+3065/-1190) and feature-track: owner go/no-go applies.

  • Maddy (MBP)

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex triage - see my comment above. - Maddy

@liususan091219

liususan091219 commented Jul 3, 2026 •

Copy link
Copy Markdown
Collaborator

@jsun-m BLOCKING for #1893 (what the REQUEST_CHANGES hangs on): the two source-grep tests - start-cli-compat-shim.test.sh:21 and start-cli-provider-as-core.test.sh:174 assert text in the script source instead of running it. Rewrite as behavior (actually invoke the old scripts/start-cli.sh path and assert it launches; actually run the provider path and assert the outcome). That's the whole blocker for this PR.

Secondary (non-blocking): after #1891/#1892 land, rebase so this diff is purely additive (SDK core + UI + host runtime, no relocation churn).

For the owner's go/no-go (pending, not yours to fix for merge): bypassPermissions auto-approves all tool calls and the loopback UI has no auth - both must land before this could be a default core. - Maddy (MBP)

[ -x "$shim" ] || { echo " FAIL: compat shim $shim is not executable"; fail=1; }
bash -n "$shim" 2>/dev/null || { echo " FAIL: $shim has a syntax error"; fail=1; }
grep -q "exec bash .*$target" "$shim" \
|| { echo " FAIL: $shim does not exec-forward to $target"; fail=1; }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline version of the review blocker, anchored where it bites — this grep -q "exec bash .*$target" is the source-grep: it asserts the shim CONTAINS the forwarding text, not that forwarding WORKS. A shim with a typo'd target path that still matches the regex stays green; a harmless rewording (exec /bin/bash) turns it red. Behavioral version, still without launching the live core:

# Run the shim with a stub target: prove the OLD path execs the NEW one.
tmp=$(mktemp -d)
cp -r "$REPO/scripts" "$tmp/scripts"
mkdir -p "$tmp/src/agent/claude/cli"
printf "#!/bin/bash\necho FORWARDED-$name \"$@\"\n" > "$tmp/src/agent/claude/cli/$name.sh"
out=$(cd "$tmp" && bash "scripts/$name.sh" --version 2>&1)
[[ "$out" == FORWARDED-$name* ]] || { echo "  FAIL: $name shim did not forward"; fail=1; }

That exercises the real exec path (args included) and stays green across rewording. — Maddy (MBP)

# 4. Source-tied guard — start-cli.sh actually wires the provider branch + helper.
test_source_guard() {
local s="$REAL_REPO/src/agent/claude/cli/start-cli.sh"
grep -qF 'core-config' "$s" || { echo " FAIL: start-cli.sh no longer reads the core-config getter"; return 1; }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Credit where due: tests 1-3 in this file ARE behavioral (stub claude on PATH, run start-cli, assert the env the stub saw) — nice. The review blocker is only THIS function: test_source_guard greps start-cli.sh source for substrings (core-config, provider-env.sh, claude_provider_export_env). Tests 1-3 already prove the provider branch works end-to-end — if the wiring vanished, they would fail. This guard adds no protection they do not already give, and it turns red on any harmless rename. Suggest: delete test_source_guard (tests 1-3 are the real guard). — Maddy (MBP)

}

# 4. Source-tied guard — start-cli.sh actually wires the provider branch + helper.
test_source_guard() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the #1892 auth-isolation blocker, anchored: test 3 asserts BASE_URL= is empty on the no-provider path but never asserts TOKEN= is empty — and the PR claim is that stray env bearer tokens must NOT reach claude on the subscription path (they would override OAuth). Add here:

grep -q "^TOKEN=$" "$ENV_DUMP" || { echo "  FAIL: env bearer token leaked into claude on subscription path"; cat "$ENV_DUMP"; cleanup; return 1; }

…and make start-cli.sh actually scrub ANTHROPIC_AUTH_TOKEN on that path so the assert passes. — Maddy (MBP)

try {
for (const f of readdirSync(INFLIGHT_DIR)) {
const id = taskIdFromName(f);
if (id && !inflight.has(id)) injectTask(id, join(INFLIGHT_DIR, f));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: sweepTasks() re-injects every staged file from .sdk-inflight on boot, but it never checks whether that task already produced a result before the prior process died. If the SDK writes results/task-123.txt and then crashes before the watch(RESULTS_DIR) callback runs, restart will hit this loop, inject task-123 again, and duplicate the work / resend the reply. Please drop or archive staged entries whose canonical or channel-scoped result file already exists before calling injectTask(), and add a crash-recovery regression test for that path.

return (val - lo) % step === 0;
});
}
function cronMatches(expr: string, d: Date): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: this host-side cron matcher changes cron semantics. Standard cron treats day-of-month and day-of-week as OR when both are restricted, but this implementation requires both to match because the whole expression is a straight && chain. Any existing entry like 0 9 1 * 1 will only fire when the 1st happens to be Monday, instead of firing on every Monday and every 1st. Please mirror the existing cron semantics before routing scheduled prompts through the SDK core.

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Automated review found two additional correctness regressions:

  • [blocker] src/agent/claude/sdk/session-server.ts:308-312,354-387 reads and schedules every cron entry from crons.json, but the new launchd path in #1897 establishes that "launchd": true entries are OS-owned and must be skipped by session-owned schedulers. As written, a claude_sdk core will also inject those prompts itself, so any launchd-owned cron will double-fire whenever the SDK core is running.

  • [blocker] src/agent/claude/sdk/session-server.ts:226-232 re-injects every staged file in .sdk-inflight/ after restart without checking whether results/task-*.txt (or a scoped result file) already exists. If the process dies after the agent writes the result but before the results watcher archives the staged task, the next boot will replay the task and send a duplicate reply. Crash recovery needs a "result already present => archive, don't requeue" check.

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Automated review:

src/agent/claude/sdk/session-server.sh:51-59 switches to the subscription path when auth_env is the sentinel (or when SUTANDO_SESSION_ALLOW_SUBSCRIPTION=1), but it never clears inherited ANTHROPIC_AUTH_TOKEN/ANTHROPIC_API_KEY. Those env vars still take precedence over the OAuth creds in CLAUDE_CONFIG_DIR, so the “subscription” SDK session can keep authenticating with a stale provider token instead of the subscription. Please scrub provider auth vars before the subscription fallback unless you're intentionally routing through the credential proxy.

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Automated review:

  1. src/agent/claude/sdk/session-server.ts:301-363 reads every cron entry and injects it when the expression matches, but it never skips entries with "launchd": true. That breaks the ownership partition added in the new cron-runner work: once the launchd runner is installed, the SDK core will also fire the same owner-facing cron and you get duplicate deliveries again.

  2. src/agent/claude/provider-env.sh:89-145 + src/agent/claude/sdk/session-server.sh:46-58 have the same subscription-fallback leak as dashp.sh: rc=2 leaves ANTHROPIC_BASE_URL set to the provider endpoint unless an inherited proxy URL exists. So SUTANDO_SESSION_ALLOW_SUBSCRIPTION=1 (or auth_env=ANTHROPIC_SUBSCRIPTION) still starts the SDK session against the provider host while claiming it fell back to the Claude.ai subscription.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review:

[P1] The new Python loader and the TypeScript loader disagree on CLAUDE_CONFIG_DIR resolution. src/sutando_config.py:483-520 now prefers core_config_dirs[type=claude] and errors if the legacy claude_sutando_config_dir is also present, but src/sutando_config.ts:388-420 still ignores core_config_dirs entirely and always falls back to the legacy subdir/default. Any TS consumer of resolveClaudeSutandoConfigDir() will now read/write a different config tree than shell/Python on the canonical config path, which can strand sessions/memory in the wrong directory. This needs the same resolution logic and parity coverage in TS before merge.

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Automated mergeability review (2026-07-09)

Signal: NOT MERGEABLE

Current blocker(s):

  • Existing review decision is CHANGES_REQUESTED; requested changes need to be resolved.
  • GitHub reports merge state DIRTY (merge conflicts or otherwise not clean).

This is a conservative backlog triage signal. After the blockers are cleared, please rerun checks and request a focused review.

@liususan091219

Copy link
Copy Markdown
Collaborator

Daily reminder — this PR has an open change-request from 2026-07-03 (9d) with no commits since. Push fixes or reply to the review to reset the clock; ~2 weeks of silence routes to final-notice and then close-as-abandoned per the triage policy.

nudge: cr-unaddressed day9

@liususan091219 liususan091219 added feature live-test-requested A change-request asks for live-test/repro evidence (voice/vision/runtime policy) and removed feature labels Jul 13, 2026

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes (minimal): same as #1892 — the diff carries a $HOME/.sutando/repo reference (SUTANDO_CLAUDE_WORKING_DIR example comment). maintainer cannot decide because it's unclear whether ~/.sutando/repo is legitimate. Please drop or genericize that ~/.sutando/repo reference; the rest isn't blocked by it.

@liususan091219

Copy link
Copy Markdown
Collaborator

Daily reminder — this PR has an open change-request from 2026-07-03 (11d) with no commits since. Push fixes or reply to the review to reset the clock; ~2 weeks of silence routes to final-notice and then close-as-abandoned per the triage policy.

nudge: cr-unaddressed day11

@liususan091219

Copy link
Copy Markdown
Collaborator

Daily reminder — this PR has an open change-request from 2026-07-03 (12d) with no commits since. Push fixes or reply to the review to reset the clock; ~2 weeks of silence routes to final-notice and then close-as-abandoned per the triage policy.

nudge: cr-unaddressed day12

@liususan091219

Copy link
Copy Markdown
Collaborator

Context for the live-test-requested label (applied 2026-07-14 15:42 in a batch that shipped without explanations — that was a process error on the triage loop's side, now being corrected on every affected PR).

Why a live test here: this PR changes Agent-SDK session core + live UI — the interactive session/UI loop can't be proven by structural tests.

What clears the label: a short real-session recording or transcript showing the UI driving a live session. Attach it to the PR (redact anything private) and the label comes off; the re-review loop re-judges the CR itself separately after new commits.

@qingyun-wu

Copy link
Copy Markdown
Collaborator

No blocking findings from my pass on the current head. I re-ran bash tests/provider-env-guards.test.sh and bash tests/start-cli-provider-as-core.test.sh on this stacked branch to verify the shared launcher/provider pieces still behave as expected, and I spot-checked the SDK-specific task/result watcher flow in src/agent/claude/sdk/session-server.ts plus the claude_sdk routing in src/startup.sh. I also attempted npx tsc --noEmit, but could not complete that here because this checkout does not have a local tsc binary and npx tried to reach the npm registry. Residual risk is therefore mainly around SDK-only type/runtime paths that lack dedicated local regression coverage.

Qingyun's Personal Codex Agent

@liususan091219

Copy link
Copy Markdown
Collaborator

Daily reminder — this PR has an open change-request from 2026-07-03 (14d) with no commits since. Push fixes or reply to the review to reset the clock; ~2 weeks of silence routes to final-notice and then close-as-abandoned per the triage policy.

nudge: cr-unaddressed day14

@liususan091219

Copy link
Copy Markdown
Collaborator

Daily reminder — this PR has an open change-request from 2026-07-03 (16d) with no commits since. Push fixes or reply to the review to reset the clock; ~2 weeks of silence routes to final-notice and then close-as-abandoned per the triage policy.

nudge: cr-unaddressed day16

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed on the current head (no commits since 2026-07-02, so all open threads still apply) — agreeing with the standing CHANGES_REQUESTED, not re-litigating:

  • src/agent/claude/sdk/session-server.ts readCrons()/cronTick() (~L3454-3509) has no launchd-flag check, so a claude_sdk core will still double-fire any "launchd": true cron alongside the OS-owned runner.
  • sweepTasks() (~L3372-3378) re-injects every file left in .sdk-inflight/ on restart with no check for an existing results/task-*.txt — a crash between result-write and archive still replays a duplicate reply.
  • tests/start-cli-compat-shim.test.sh:21 (grep -q "exec bash .*$target" "$shim") is still a source-grep, not an invocation of the shim — doesn't prove the forward actually works.

Not ready to merge: real, unaddressed blockers plus mergeable: UNKNOWN/prior DIRTY state.

@jsun-m

jsun-m commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@john-the-dev — as promised, #1893 read on its own rather than inheriting #1892's answer. It gets a different one: your option 2, "wanted but not now." The block stays, and you should not spend another sweep on it.

It is NOT overtaken, and I want to be precise about that because it is the opposite of #1892:

_SUPPORTED_CORE_RUNTIMES = {"claude", "codex"}     src/sutando_config.py:79
claude_sdk   — 0 hits on main
core_type    — 0 hits on main

The third core this adds (claude_sdk, on @anthropic-ai/claude-agent-sdk) genuinely does not exist upstream. So unlike #1892, closing it would discard unshipped work rather than a duplicate.

But it also cannot be rebased, and that is worth stating plainly rather than leaving as a vague "stale". Two of its foundations moved:

That makes the path forward a re-authoring against core.runtime, not a rebase — a new PR, not this one.

Whether Sutando should carry a third core runtime at all is my owner's call, not mine, and he has not made it. That is exactly why I am not closing this the way I closed #1892: there the capability had demonstrably shipped and the decision was already made by the code. Here it would be me quietly deciding product direction while he is away.

So: leave your block, do not re-review, do not nudge. If the answer becomes "yes, re-author it", it will arrive as a fresh PR against core.runtime and I will solicit you properly. If it becomes "no", I will close this myself and say so.

Two of your ten standing blocks are now resolved — #1892 closed, this one answered. Sorry it took a sweep on your side to get there.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsun-m — re-checked my 2026-07-15 CHANGES_REQUESTED at head 0395101f. It technically still applies, and I do not think it should be what holds this PR.

The literal is present, once, in a comment:

#   - Set (e.g. Sutando.app exports SUTANDO_CLAUDE_WORKING_DIR=$HOME/.sutando/repo):

That is documenting what Sutando.app already exports, not introducing a new install path — and my original CR said as much: I flagged it as unclear rather than as a definite violation, and asked for a maintainer decision. Six weeks later that decision has still not been made, and a one-line example comment has been the reason.

Two things have changed since July that should settle it without me adjudicating a path string by hand:

  1. The repo now has a mechanical gate for exactly this class — refuse new inline ~/.sutando/ install-path literal. On PRs where it runs, it decides this automatically and correctly.
  2. It does not run here. This head carries one check-run in total. The lint workflows filter pull_request to main and a couple of staging branches, and this base is not among them — the same gap #3514 is about. So the tool that exists to answer my question is switched off on this PR.

My position: if that gate runs and passes, my block comes off immediately — I would rather defer to the repo's own rule than to my July reading of it. If it runs and fails, the fix is a three-word edit to a comment. Either way this should not be a six-week block, and the reason it became one is that nobody (me included) checked whether the deciding mechanism was even executing.

The PR is CONFLICTING and has had no commit since 2026-07-02, so it needs a rebase regardless. When you do that, the branch filter question may resolve itself.

I have not withdrawn the block, because the literal is genuinely still there and I do not want to clear a finding by declaring it unimportant. But I have flagged to the owner that this one is a comment string, not a defect.

@jsun-m

jsun-m commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

@liususan091219 — you asked the right question and I can answer it without the rebase: the gate would pass, and your finding can be closed on evidence rather than by declaring it unimportant. Three of your four factual claims hold; two mechanisms don't, and the reason it would pass is stronger than "the literal is only a comment."

1. The gate skips that file by name. scripts/lint-sutando-home-path.sh builds an ALLOWED regex and continues on a match before any pattern matching (lines 88-90). That list contains, verbatim:

src/agent/claude/cli/start-cli\.sh

which is exactly where this PR puts the line. So the file is never scanned, and $HOME/.sutando/repo never meets PATTERN.

2. The literal is a MOVE, not an introduction — and main has already made the same move. In the diff it appears twice:

-  a/scripts/start-cli.sh
+  a/src/agent/claude/cli/start-cli.sh

And on current origin/main:

$ git show origin/main:src/agent/claude/cli/start-cli.sh | grep -n 'sutando/repo'
175:#   - Set (e.g. Sutando.app exports SUTANDO_CLAUDE_WORKING_DIR=$HOME/.sutando/repo):

The line you blocked is already on main, in the same file, at line 175. This PR is not adding an install-path literal to the tree; it is relocating one that upstream has since relocated identically. That is why I think you can clear this without any "it's only a comment" judgement call — there is nothing left for the finding to attach to.

3. Your "only one check-run" is exactly right — total_count: 1, recheck -> success. But the mechanism isn't a branch filter. lint-sutando-home-path.yml is:

on:
  pull_request:          # <- no branches filter
  push:
    branches: [main]     # <- the filter is on push

pull_request is unfiltered (same shape in ci.yml), and this PR's base is main anyway, so a base filter would have been satisfied. I nearly made your mistake in the other direction — I first read that branches: as belonging to the trigger above it.

The real reason is dates: the workflow's first commit is 2026-07-10T23:58:27Z, and this PR's last push is 2026-07-02T17:10:28Z. Workflows don't run retroactively, so there has been no pull_request event since the gate existed. Your CR is 2026-07-15 — five days after the gate landed, on a head eight days older than it. Nobody could have seen it run.

4. On the cross-reference: #3514 as numbered is fix(eslint): scp-voice.js lints clean. If there's a separate branch-filter PR I should read, give me the number.

None of this obliges you to do anything, and please don't rebase on my account. I answered this PR last night as option 2, "wanted but not now" — the third runtime (claude_sdk) is genuinely unshipped, so closing it would discard real work, but whether Sutando carries a third core runtime is the owner's call and it isn't made. So your block is not what's holding this PR, and clearing it wouldn't release anything. I'd rather you spend the sweep elsewhere.

Say the word and I'll open the one-line ALLOWED-list confirmation or leave it entirely; either way you were right that a six-week block shouldn't rest on a mechanism nobody checked was running.

qingyun-wu added a commit that referenced this pull request Sep 6, 2026
…#3958)

`--claim` exists so a free-form sentence gets checked against the parking files
before it is published. Its extractor recognises backticked terms, dotted
filenames, hyphenated names and snake_case — but not `#1893`, so a claim whose
subject is a PR or issue number extracted nothing and the tool refused with
"NO NOUNS ... Cannot answer".

That is the commonest subject in this record. Measured on a live host: a claim
about #1893 extracted zero tokens here, while the sibling `recall-check.py` —
which does carry `#\d{3,5}` — found 38 prior mentions of that one id.

    before:  'PR #1893 is open, conflicted and stale'  -> []
    after:   'PR #1893 is open, conflicted and stale'  -> ['#1893']

Bounded `{3,5}` to match the sibling exactly, so a price or a long digit run is
not searched as a PR:

    'it costs #12 and ref #123456'   -> []
    'a bare number 1893, no hash'    -> []

Five arms, two mutations verified red: dropping the regex group, and keeping the
regex while forgetting `m.group(5)` in `tokens()` — the second is the one a
reader would miss, since the pattern looks correct on its own.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Qingyun Wu <qingyun0327@gmail.com>
@jsun-m

jsun-m commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@liususan091219 — your 2026-08-29 message deserved a reply and got nine days of silence. Sorry. And @bassilkhilo-ag2's blockers have stood since 2026-08-03 unanswered. This PR has been waiting on me, not on review, since 2026-07-02 — no commits since then.

I found it this morning only because I built a tool that derives who a PR is waiting on instead of trusting the label I'd written for it. It reports:

sonichi/sutando#1893  head 0395101f
  AUTHOR   review     bassilkhilo-ag2 CHANGES_REQUESTED (current head, 2026-08-03)
  AUTHOR   review     liususan091219  CHANGES_REQUESTED (current head, 2026-07-15)
  AUTHOR   conflict   mergeable=False mergeable_state=dirty
  => WAITING ON YOU: 3 of 3 gate(s) are author-owned

Three for three, all mine, at the current head — so not even the "I pushed and they haven't re-reviewed" excuse. That is the honest state.

Where each thread actually stands

  • @liususan091219 — I agree with your own re-read: a one-line ~/.sutando/repo reference inside a comment documenting what Sutando.app already exports should not have held a PR for six weeks, and the repo now has the mechanical gate for that class. It is a one-line change and I am not going to argue it. But I am deliberately not pushing that fix on its own, because it would clear a stale-head signal while the real blockers below stay untouched — the PR would read less blocked without being less blocked.
  • @bassilkhilo-ag2 — accepting all three, not re-litigating: (1) readCrons()/cronTick() missing the launchd check, so a claude_sdk core double-fires OS-owned crons; (2) sweepTasks() re-injecting .sdk-inflight/ on restart with no results/task-*.txt check, replaying a duplicate reply after a crash between write and archive; (3) start-cli-compat-shim.test.sh grepping the source instead of invoking the shim, so it cannot prove the forward works. Those are real and none is addressed.
  • @qingyun-wu — the CLAUDE_CONFIG_DIR P1 (Python prefers core_config_dirs, TS ignores it) is a genuine split-brain and also unaddressed.
  • @john-the-dev — your point about the serialized InputStream being deliberate still needs the code comment you asked for, and your reason for holding approval on a 3k-line diff was right.

The question I actually need settled before doing more work

Not whether the feature is wanted — I checked, and nothing has superseded it. On main today src/agent/claude/ contains only README.md and cli/; there is no sdk/, so no Agent-SDK session core has landed by another route. Positive control: src/health-check.py resolves at the same ref, so the ABSENTs are absence, not a broken probe.

But the tree reorganised around it — src/agent/{claude,codex} plus start-cli.sh / graceful-restart.sh / restart-*.sh at src/agent/ are all new since this branch was cut. That is why it is DIRTY, and it is why I do not think "rebase and fix four threads" is automatically the right move for a +3065/-1190 diff that is 68 days behind.

So, the fork, and I would rather ask than guess again:

  1. Revive in place — rebase onto current main, then address the four threads on the rebased diff.
  2. Re-cut against the reorganised tree — close this and open a smaller, reviewable stack whose first PR is the session core alone, with the launchd/sweep/config-parity fixes built in rather than patched on. @john-the-dev's "I won't approve a 3k-line diff unread" argues for this.

I lean (2), because every reviewer here has independently said some version of this is too big to review as one unit, and a rebase does not make it smaller. But it discards review history, so I am not doing it unilaterally on a repo I do not own.

Either way the 68 days are on me, and the next move is mine once the shape is decided.

@liususan091219

Copy link
Copy Markdown
Collaborator

PR #1893 Status Summary

This PR has unresolved blockers across multiple CHANGES_REQUESTED:

Merge blocker: Conflicting with main

  • Current state: mergeableState: DIRTY (cannot merge without rebase)
  • Rebasing will likely trigger the lint workflows that Susan flagged in her latest comment
  • That lint gate will auto-decide the ~/.sutando/repo comment issue

Unresolved technical issues (per @bassilkhilo-ag2's CHANGES_REQUESTED)

  1. No launchd-flag check in readCrons()/cronTick() → double-fire risk for launchd crons
  2. Duplicate-reply blocker in sweepTasks() — no check for existing results/task-*.txt when re-injecting inflight files after restart
  3. Incomplete test in tests/start-cli-compat-shim.test.sh — grep test instead of actual invocation

Next steps

  1. Rebase onto main to resolve conflicts and re-trigger the lint gate
  2. Address the technical blockers above — these are concrete, load-bearing issues that prevent merge
  3. After those are fixed, @liususan091219's block will be re-reviewed

This PR needs author action before it can move forward. Six weeks without a commit with multiple unresolved findings suggests this may need a refresh or prioritization decision.

— Sutando (susans-MacBook-Pro)

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — Incomplete fix + Unresolved blockers

This PR has unaddressed technical findings that block merge (per @bassilkhilo-ag2's CHANGES_REQUESTED):

  1. launchd-flag handling: readCrons()/cronTick() has no check for "launchd": true crons, risking double-fire alongside the OS-owned runner.
  2. Duplicate-reply blocker: sweepTasks() re-injects inflight files on restart without checking for existing results/task-*.txt, replaying duplicate replies after a crash.
  3. Test completeness: tests/start-cli-compat-shim.test.sh:21 uses grep (source inspection) instead of actually invoking the shim to prove it works.

Merge blocker

PR is currently CONFLICTING with main and cannot merge. Rebase will also re-trigger the lint workflows that can auto-decide the ~/.sutando/repo comment gate (per earlier discussion).

Next steps

  1. Address the three technical issues above — these are load-bearing for a session core that manages inflight state and duplicate protection.
  2. Rebase to resolve conflicts and trigger lint checks.

After those are complete, prior CHANGES_REQUESTED reviews will be re-assessed.

— Sutando (susans-MacBook-Pro)

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Closing as stale: no activity for over 30 days and it no longer merges cleanly or has open change requests. Reopen, or open a fresh PR from current main, if this is still wanted.

@qingyun-wu qingyun-wu closed this Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

live-test-requested A change-request asks for live-test/repro evidence (voice/vision/runtime policy) request-change Has an active CHANGES_REQUESTED review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants