Add a name on ai client and resume from it #6 #7
Labels
No labels
ai::retry
ai::retry_once
ai::retry_until_green
ai::validate_plan
status::blocked
status::done
status::functional_analysis
status::in_ci
status::in_development
status::in_review
status::technical_analysis
status::waiting_info
status::waiting_validation
type::analysis
type::bug
type::question
type::task
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Alchemic.Studio/Alfred#7
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Based on the work of issue 1 "Add a name on client and resume from it", integrate this in the developpment process.
When you start a ai client agent like claude code, codex, etc... Attache to it a name isued from the branch name and on each run, resume from it.
Functional analysis
Source of truth. Issue 1 specified this behaviour in
docs/specification.md§16.5 "Named sessions and resume", together with the data model (§ llm_sessions:session_name,native_session_id,resumed,resume_fallback), the settingAGENT_SESSION_RESUME(defaulttrue, organization and project scope), runtime detection (§16.6) and the test matrix. None of it is implemented yet: those names exist only in the specification. This issue makes the engine apply it.Interpretation. "Integrate it in the development process" means every local-agent run of an issue mailbox, in every phase (analysis, development,
ci_fix,review_fix,conflict_resolution,answer), names its CLI session after the issue branch and resumes the session left by the previous run.Expected behaviour
AGENT_SESSION_RESUMEis on (project, else organization, else defaulttrue); the bound adapter declaresresumableSessionsfor the installed CLI version; the run executes in a container runner; the target is an issue mailbox. If any one is false, the request, mounts and session row are exactly what they are today.{BRANCH_PREFIX}issue-{iid}-{slug}, frozen inmailboxes.branch_nameat the first run. Analysis runs use it too, even though the branch does not exist yet. When the CLI supports a human-readable session label, the adapter sets it to this name.LlmResult::$sessionId. The engine stores it inllm_sessions.native_session_id. On the next run it reads the id back from the database only (latest row of the same mailbox and sameadapter_idwith a non-null id), never from the session state directory.resume_fallback = no_prior_session.adapter_changed.state_missing,unsupported_version,refused), the adapter runs the CLI once more in a fresh session under the same name, within the same run and the remaining timeout. The fallback never fails the run, never counts as an LLM retry, and changes no status, counter, label or comment.{base}/.agent-sessions/{organization}-{project}-issue-{iid}/{adapter_id}/, mounted at the adapter'ssessionStatePath. It is removed at mailbox cleanup and left untouched when the setting is turned off.Acceptance criteria
AGENT_SESSION_RESUMEsetting (defaulttrue, organization and project override).resumableSessionsandsessionStatePath, declared per local adapter (Claude Code:--session-id <uuid>for a fresh session and--resume <id>to resume; Codex, OpenCode, Copilot CLI and Grok Build each according to their CLI). The capability is lowered by runtime detection when the installed version falls outside the verified range.llm_sessionsgainssession_name,native_session_id,resumedandresume_fallback. They stay null or false whenever the feature does not apply.no_prior_session. Adapter changed between phases → fresh session,adapter_changed. Provider or model change → resumes.FakeLlmAdaptergainsresumableSessions, reports a native id and can simulate a refused resume). Playwright e2e covers the run screen fields. All of them run on both GitLab CI and Forgejo/GitHub Actions.Thanks @SebastienDenooz, I am on it. I will post my analysis here.
Plan
Implementation plan: named agent sessions and resume (§16.5)
Current state
LlmRequest,LlmResult,LlmCapabilitiesandDetectionhave no session fields.llm_sessionsandagent_installationshave no session columns. There is noAGENT_SESSION_RESUMEsetting.RunExecutor::createWorktree()(app/Runs/Execution/RunExecutor.php:308) setsmailboxes.branch_nameonly for read-write phases. A read-only first run (analysis) therefore gets no name. §7.3 asks for the name to be fixed at the first run, whatever its phase, while the branch itself is only created fromin_developmentonward.HOME=/home/agentas a tmpfs. Claude Code writes its transcript there today, and it is lost when the container is removed.Design
1. Data contracts (
app/Llm/Data)SessionRef(final readonly):name,?resumeId,stateDir(host path),?freshReason(no_prior_session|adapter_changed, null whenresumeIdis set).App\Enums\ResumeFallback:NoPriorSession,AdapterChanged,StateMissing,UnsupportedVersion,Refused.LlmRequest: add?SessionRef $session = nullas the last argument, so existing calls (SmokeTest, the tests) don't change.LlmResult: add?string $sessionId = null,bool $resumed = false,?ResumeFallback $resumeFallback = null.LlmCapabilities: addbool $resumableSessions = falseand?string $sessionStatePath = null. The constructor throws ifresumableSessionsis true and the path is null or not underSandboxSpec::HOME.Detection: addbool $resumableSessions = false.2. Setting
SettingsCatalogue:$this->add('AGENT_SESSION_RESUME', SettingType::Bool, true, $tenant);(organization and project scope, likeREQUIRE_PLAN_VALIDATION).3. Detection and the installation capability
config/alfred.php: addagents.{id}.resume_versionsper adapter, the range verified for non-interactive resume. Empty means never.LocalAgentAdapter::inspect()reportsresumableSessions = capabilities()->resumableSessions && Semver::satisfies(version, resume_versions). It is always false for a host runner.add_resumable_sessions_to_agent_installations_table:resumable_sessionsboolean, default false.AgentDetector::detectIn()stores it. Show it on the agents screen next to the version.4. Adapters
LocalAgentAdapter::invoke():SessionRef, the code path stays exactly as today.SessionRef, it records the start time (hrtime) and builds the command through a new hook,command(..., ?SessionMode $mode)(Fresh(id|null)orResume(id)).resumeFailure(ExecResult): ?ResumeFallbackclassifies the output. It returns null when the cause is ambiguous (§16.5 trap), and the failure then keeps its usualOutcome.Freshwith the remaining timeout (timeoutMs - elapsed). If no time is left, the result isTimeout.sessionId(ExecResult, SessionMode): ?string, reads the native id from the output and falls back to the id the adapter chose.resumableSessions: true,sessionStatePath: /home/agent/.claude/projects.--session-id <random uuid v4>. It must be random, not derived from the name, so that a fallback after corrupted state never collides with an existing id.--resume <id>.session_idin the JSON output.resumeFailurematches "No conversation found with session ID…" →StateMissing.HOST_OPTIONS(--no-session-persistence) are unchanged.sessionStatePath: /home/agent/.codex/sessions. This keepsauth.jsonandconfig.tomlout of the persisted directory.codex exec --json, id taken from thethread.started/ session event.codex exec resume <id>.--session <id>, id from the JSON output) and Copilot CLI (--resume <id>): declared with their ownsessionStatePath(sessions subdirectory only) and recognition patterns. Theirresume_versionsranges must be checked against the CLIs during implementation. If either stores credentials next to its sessions, it staysfalse.resumableSessions: false;resume_versionsempty.ApiLlmAdapter: unchanged (defaults to false).FakeLlmAdapter:withResumableSessions(string $path = '/home/agent/.fake/sessions').resumeId, or a new uuid for a fresh session.refuseNextResume(ResumeFallback $reason = Refused)simulates the in-adapter fallback and returnsresumed=falsewith that reason.$request->session.5. Engine (
RunExecutorplus a new class)App\Runs\Execution\SessionResolverwithresolve(Run, Mailbox, Project, LlmAdapter, AgentInstallation?, SandboxSpec): ?SessionRef. It returns null unless all of these hold:sandbox->kind === RunnerKind::Image;capabilities()->resumableSessions;installation->resumable_sessions;settings->bool('AGENT_SESSION_RESUME', org, project);sessionStatePathdoes not overlap a credential mount (see Risks).llm_sessionsrow joined torunswith the samemailbox_id, the sameadapter_idand a non-nullnative_session_id, excluding the current run.native_session_idfor another adapter on the same mailbox, the reason isadapter_changed; otherwise it isno_prior_session.{repos_base_dir}/.agent-sessions/{org-slug}-{project-id}-issue-{iid}/{adapter_id}, following theGitRepository::worktreePath()convention.AgentSessionStoreclass (pathFor,ensure,removeMailbox), shared with the cleanup.createWorktree():branch_nameon the first run of an issue mailbox, whatever the phase.branch_namefor read-write phases, or for read-only phases whenorigin/{branch}exists (newGitRepository::remoteBranchExists()). Otherwise it uses the temporaryalfred/run-{id}.in_development" true and stops analysis from leaving a local branch behind.SandboxSpecFactory::forRun()takes?SessionRef(or the host directory and the container path) and fills a newSandboxSpec::$sessionMount([host => container], read-write even in read-only phases).sessionStatePath, withuid=,gid=,mode=0700, so the CLI can still write the rest of its home directory (see Risks).CliContainerRuntime::createArguments()adds--volume host:container:rw.FakeContainerRuntimerecords it.RunExecutor::steps():forRun).session_namebefore the invocation.SessionRefinLlmRequest.recordResult():native_session_id = result->sessionId,resumed = result->resumed,resume_fallback = result->resumeFallback ?? ref->freshReason.SessionRefwas sent. They are also written when the outcome is a failure, so a timed-out session can still be resumed next time.llm_retries: it lives entirely inside the adapter.MailboxCleanup: callAgentSessionStore::removeMailbox()inside the existing try block. The setting being off leaves the directories alone.6. Data model
add_session_columns_to_llm_sessions_table:session_namestring nullable,native_session_idstring nullable,resumedboolean default false,resume_fallbackstring nullable.runs(mailbox_id)only if it is missing.LlmSessionmodel: add casts (ResumeFallbackenum, bool).7. Run screen
RunController::data()(context tab): addsession_name,native_session_id,resumed,resume_fallbackto the selected columns.resources/views/runs/partials/tab-context.blade.php: show a Session row only whensession_nameis set: the name, then the native id in<code>, then "resumed" or "fresh (reason)". Each part gets adata-testhook.Tests
Unit
SessionResolverTest: each condition off returns null (API, adapter capability off, installation flag off, host runner, MR mailbox, setting off at organization scope, setting off at project scope, credential overlap). Prior id for the same adapter gives resume. No prior id givesno_prior_session. A prior id only for another adapter givesadapter_changed. A provider or model change still resumes.ClaudeCodeAdapterTest,CodexAdapterTest,OpenCodeAdapterTestandCopilotCliAdapterTest: exact argv for fresh, resume and no session (the last one identical to today's snapshot); native id parsing;resumeFailurepatterns; fallback re-exec with the remaining timeout; an ambiguous failure gives no fallback; host runs never get session options.LocalAgentAdapterdetection:resume_versionslowers the capability; host detection always gives false.SandboxSpecFactoryandCliContainerRuntime: the session mount is read-write in read-only phases and absent without aSessionRef; the parent tmpfs options are present.LlmCapabilitiesrejectsresumableSessionswithout a path.Feature (
FakeLlmAdapterplusFakeContainerRuntime)resumeIdequal to the first run'snative_session_id, and the row hasresumed=true.no_prior_session. An adapter change between analysis and development givesadapter_changed. Changing the binding's model still resumes.resume_fallback=refused,llm_retriesis unchanged, no forge comment, no label change.branch_namewithout creating the branch (temporary worktree branch, nothing pushed).MailboxCleanupremoves the state directory. Turning the setting off keeps it.AgentDetectorstoresresumable_sessions.Browser (Pest + Playwright,
tests/Browser/Runs/RunsTest.php)CI:
php artisan testalready runs unit, feature and browser tests in.gitlab-ci.yml,.github/workflows/ci.ymland.forgejo/workflows/ci.yml. No pipeline change is needed beyond checking that the new tests run in all three.Risks and decisions
AGENT_CREDENTIAL_MOUNTS: the documented exampleclaude-code=/srv/claude:/home/agent/.claudecontains.claude/projects. Docker would create the mountpoint inside the read-only host credential directory. Decision: ifsessionStatePathequals, contains or sits under a credential mount, resume does not apply to that run. A warning is logged and the agents screen shows the reason. Credentials are never weakened./home/agent/.claude) as root inside the home tmpfs, which would stop the CLI from writing its other files. The parent tmpfs owned by the worker uid:gid prevents this. This needs to be checked once with a real Docker runner and a realclaudebinary, beyond the fakes.resume_versions. Any unverified adapter staysfalse.CleanDeletedBranchresetsbranch_name: a later run gets a newly computed name. Resume is unaffected, because the prior id is looked up by mailbox and adapter, not by name.Commits (Conventional Commits)
feat(llm): add session contracts, capability and detection of resumable sessionsfeat(settings): add AGENT_SESSION_RESUMEfeat(runs): resolve, mount and record named agent sessionsfeat(llm): resume Claude Code, Codex, OpenCode and Copilot CLI sessions with silent fallbackfeat(mailboxes): remove agent session state at cleanupfeat(runs): show the agent session on the run screentest: cover session resume across adapters, runs and the run screenThe implementation plan is ready for validation. Add the ai::validate_plan label to let me start.
Plan validated by @SebastienDenooz. Starting the development.
The status label status::in_development is managed by me: I restored it. Use the command labels to steer my work.
The status label status::in_development is managed by me: I restored it. Use the command labels to steer my work.
Retrying
development.Retrying
development.Retrying
development.