mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
fix(agent): verify staged text on a live codex pane and refuse popups
Stage now re-reads the pane after typing (every 50 ms, up to 1 s) and issues a receipt only for a frame with the same generation, the input revision its own write moved, and a composer hash equal to the message's. Codex repaints 30 to 60 ms after the keys, so the first capture was often stale and stage refused with the text already typed. Every refusal after typing now says the message may be in the composer and points at the Ctrl+U recovery. A visible-frame capture reads the screen and the caret in one tmux invocation, with the old two calls as the fallback. Codex draws its suggestion popups above the composer and keeps the footer, so a staged /stat verified and submit ran /status. Stage now refuses a leading / or ! and a last word starting with @ or $ before typing, and stage and submit refuse a frame with popup rows just above the composer. The pattern and characters are registry data (composerPopupLine, composerCommandPrefixes, composerCompletionSigils), read from the codex 0.162.0 bottom_pane sources. Submit waits until 250 ms after the staged keys: codex turns an Enter within 120 ms of a typed burst into a newline. POST .../input answers 409 with its seq returned while a guarded submit holds the input lease, instead of 200 for bytes it never wrote. Also align the attribution memo comment with the code (a verified file whose incremental scan fails is not re-walked, since the full walk reads a superset), and list stage as a fromUser source in CLAUDE.md.
This commit is contained in:
1 parent
c78635a3d3
commit
2832a037d6
27 files changed
+849
-74
No files matched your search
@@ -14,7 +14,13 @@ import type { SessionPort, InfraPort } from '../ports/index.js';
|
||||
import { getCli } from '../../config/cli-registry/registry.js';
|
||||
import { findExactCodexConversation, resolveEffectiveCodexHome } from '../../agent-codex-identity.js';
|
||||
import { InspectionReader, InspectionConflict, boundedUtf8Tail } from '../../agent-inspection.js';
|
||||
import { GuardedStageStore, GuardedInputConflict, type GuardedInputTarget } from '../../agent-staging.js';
|
||||
import {
|
||||
GuardedStageStore,
|
||||
GuardedInputConflict,
|
||||
guardedComposerChrome,
|
||||
MAY_BE_TYPED,
|
||||
type GuardedInputTarget,
|
||||
} from '../../agent-staging.js';
|
||||
import { ApiErrorCode, createErrorResponse, type ApiResponse } from '../../types.js';
|
||||
import { findSessionOrFail, getAuthUser, parseBody, readJsonConfig, SETTINGS_PATH } from '../route-helpers.js';
|
||||
import { AgentInspectQuerySchema, AgentStageSchema, AgentSubmitSchema } from '../schemas.js';
|
||||
@@ -51,6 +57,7 @@ export function registerAgentSessionRoutes(app: FastifyInstance, ctx: SessionPor
|
||||
const generation = (session: Session) => session.inspectionGeneration;
|
||||
const target = (session: Session): GuardedInputTarget => ({
|
||||
id: session.id,
|
||||
chrome: () => guardedComposerChrome(getCli(session.mode)),
|
||||
snapshot: () => {
|
||||
if (ctx.sessions.get(session.id) !== session) return null;
|
||||
const snapshot = session.guardedInputSnapshot?.();
|
||||
@@ -153,7 +160,7 @@ export function registerAgentSessionRoutes(app: FastifyInstance, ctx: SessionPor
|
||||
ApiErrorCode.CONFLICT,
|
||||
error instanceof GuardedInputConflict
|
||||
? error.message
|
||||
: 'Stage delivery failed; inspect the pane before retrying'
|
||||
: `Stage delivery failed or ambiguous; ${MAY_BE_TYPED}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2077,6 +2077,24 @@ export function registerSessionRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
// A guarded submit (agent-staging.ts) holds the input lease for the few
|
||||
// milliseconds of its conditional Enter, and every write path refuses while it
|
||||
// does. Say so instead of answering 200 for bytes that were never written, and
|
||||
// give the seq back so the browser's durable queue (which keeps a frame on any
|
||||
// 409) delivers it once the lease is released. Nothing from here to the write
|
||||
// below awaits, so the lease cannot be taken in between.
|
||||
if (!duplicate && session.guardedSubmitInFlight === true) {
|
||||
if (tagged) session.forgetInputSeq(clientId as string, seq as number);
|
||||
return reply
|
||||
.code(409)
|
||||
.send(
|
||||
createErrorResponse(
|
||||
ApiErrorCode.CONFLICT,
|
||||
'A guarded submit is in progress on this session; nothing was written. Retry the input.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Only a waiting request pays for the tmux probe: the browser's plain input path
|
||||
// (thousands of calls per session) must stay exec-free.
|
||||
const workerDead = wantsWait && workerIsDead(ctx.mux, session);
|
||||
|
||||
Reference in new issue
Block a user