Skip to content

Commit bd5ceec

Browse files
authored
Merge pull request #20 from beyond5959/dev
Dev
2 parents 5965092 + f3bbf35 commit bd5ceec

40 files changed

+3728
-202
lines changed

PROGRESS.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,24 @@ This file is the source of milestone progress, validation commands, and next act
1111

1212
- `Post-M8` ACP multi-agent readiness and maintenance.
1313

14-
## Latest Update (2026-03-09)
14+
## Latest Update (2026-03-11)
15+
16+
- `Post-M8` codex session identity and replay normalization completed:
17+
- fixed fresh Codex `New session` persistence so ngent no longer stores provisional runtime ids like `session-1` as the thread session binding when a durable `_meta.threadId` is not yet available.
18+
- deferred initial `session_bound` persistence/emission for fresh Codex sessions until a stable session id can be resolved after the first prompt, then updated in-memory and persisted thread `agentOptions.sessionId` with the durable id.
19+
- normalized Codex transcript replay by filtering bootstrap user messages injected by the desktop wrapper (`AGENTS.md` / `environment_context`) and extracting the actual user request from known wrapper formats:
20+
- `[Conversation Summary] ... [Current User Input]`
21+
- `# Context from my IDE setup: ... ## My request for Codex:`
22+
- verified with real local Codex and Playwright against `http://127.0.0.1:8687/`:
23+
- `New session` now produces distinct stable Codex session ids and no longer mixes first-session messages into the second-session chat.
24+
- switching between the two replayed sessions in the Web UI now shows only the expected user/assistant pairs.
25+
- direct `session-history` API responses for the repro sessions now return cleaned transcript messages.
26+
- validation:
27+
- pass: `go test ./internal/agents/codex -run 'Test(ParseSessionTranscriptMessage|CodexShouldDeferInitialSessionBinding|NormalizeCodexSessionListResultUsesStableThreadID|CodexSessionMatchesIDAcceptsStableAndRawIDs|CodexStableSessionIDFallsBackToRawSessionID)$' -count=1`
28+
- pass: `cd internal/webui/web && npm run build`
29+
- pass: `go test ./...`
30+
31+
## Previous Update (2026-03-09)
1532

1633
- Kimi CLI ACP integration completed:
1734
- implemented `internal/agents/kimi` with one-turn ACP stdio lifecycle and fail-closed permission handling.
@@ -574,3 +591,33 @@ This file is the source of milestone progress, validation commands, and next act
574591
- 2026-03-09: unified ACP message-chunk constant usage across stdio providers by removing per-provider `updateTypeMessageChunk` definitions and reusing `agents.ACPUpdateTypeMessageChunk`.
575592
- 2026-03-09: hid the Web UI Reasoning switch when the active agent exposes fewer than two reasoning choices, so agents without switchable reasoning no longer show a dead control.
576593
- 2026-03-09: switched Kimi model/reasoning catalog queries to local `config.toml` when available, so startup catalog refresh and thread config/model operations no longer create empty Kimi sessions; real prompt turns still use ACP.
594+
- 2026-03-11: added opt-in `--debug` startup flag; when enabled, stderr now emits sanitized `acp.message` traces for ACP stdio and embedded-runtime request/response traffic, including session prompts, updates, and permission flows.
595+
- 2026-03-11: added ACP session browsing/resume support across built-in agents:
596+
- introduced shared agent session abstractions for `session/list`, bound-session reporting, and initialize capability parsing.
597+
- built-in providers now:
598+
- list sessions through ACP `session/list` when supported.
599+
- load persisted `agentOptions.sessionId` through ACP `session/load` before prompting.
600+
- report the effective session id back to HTTP turns so the server can persist it.
601+
- added `GET /v1/threads/{threadId}/sessions` with cursor passthrough and graceful `supported=false` fallback for agents without ACP session-history support.
602+
- turn SSE now emits `session_bound`, and the server persists the thread session id without closing the active provider.
603+
- once a thread is bound to an ACP session, prompt building skips local recent-turn injection to avoid duplicating already-loaded ACP context.
604+
- Web UI now renders a right-side session sidebar with first-page load, `Show more`, active-session highlighting, and `New session` reset.
605+
- executed validation:
606+
- pass: `cd internal/webui/web && npm run build`
607+
- pass: `go test ./internal/httpapi -run 'TestThreadSessionsListEndpoint|TestTurnSessionBoundPersistsSessionIDAndSkipsContextInjection' -count=1`
608+
- pass: `go test ./...`
609+
610+
- 2026-03-11: fixed Web UI session playback when selecting an existing ACP session from the right sidebar.
611+
- the active chat view now treats `(threadId, sessionId)` as its render scope instead of refreshing only on `threadId` changes.
612+
- `loadHistory()` now filters locally persisted turns by each turn's `session_bound` event so the center chat panel replays the selected session's ngent-recorded turns instead of leaving the previous session on screen.
613+
- session changes reported mid-stream by `session_bound` defer the full chat refresh until the active turn completes, so the live streaming bubble is not destroyed.
614+
- executed validation:
615+
- pass: `cd internal/webui/web && npm run build`
616+
- pass: `go test ./...`
617+
618+
- 2026-03-11: fixed Web UI history replay for legacy session threads whose `/history` data lacks per-turn `session_bound` events.
619+
- session-scoped history filtering now falls back to showing all turns when a thread has no annotated session markers at all, instead of rendering an empty chat pane despite non-empty `/history`.
620+
- when a thread has exactly one annotated session, the selected session view also keeps older unannotated turns so pre-annotation history is still visible for that same session.
621+
- executed validation:
622+
- pass: `cd internal/webui/web && npm run build`
623+
- pass: `go test ./...`

cmd/ngent/main.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func main() {
4646

4747
portFlag := flag.Int("port", 8686, "server listen port (1-65535)")
4848
allowPublic := flag.Bool("allow-public", false, "allow listening on public interfaces (default false for loopback-only)")
49+
debugFlag := flag.Bool("debug", false, "enable verbose debug logs, including ACP request/response payloads on stderr")
4950
authToken := flag.String("auth-token", "", "optional bearer token for /v1/* endpoints")
5051
dbPath := flag.String("db-path", defaultDBPath, "sqlite database path")
5152
contextRecentTurns := flag.Int("context-recent-turns", 10, "number of recent user+assistant turns injected into each prompt")
@@ -55,6 +56,13 @@ func main() {
5556
shutdownGraceTimeout := flag.Duration("shutdown-grace-timeout", 8*time.Second, "graceful shutdown timeout for active turns")
5657
flag.Parse()
5758

59+
logLevel := slog.LevelInfo
60+
if *debugFlag {
61+
logLevel = slog.LevelDebug
62+
}
63+
logger = observability.NewJSONLogger(logLevel)
64+
observability.ConfigureACPDebug(logger, *debugFlag)
65+
5866
codexRuntimeConfig := codexagent.DefaultRuntimeConfig()
5967
codexPreflightErr := codexagent.Preflight(codexRuntimeConfig)
6068
opencodePreflightErr := opencodeagent.Preflight()
@@ -108,6 +116,9 @@ func main() {
108116
if claudePreflightErr != nil {
109117
logger.Warn("startup.claude_unavailable", "error", claudePreflightErr.Error())
110118
}
119+
if *debugFlag {
120+
logger.Info("startup.debug_enabled", "acpTrace", true)
121+
}
111122
agents := supportedAgents(codexAvailable, opencodeAvailable, geminiAvailable, kimiAvailable, qwenAvailable, claudeAvailable)
112123

113124
listenAddr, port, err := resolveListenAddr(*portFlag, *allowPublic)
@@ -148,12 +159,14 @@ func main() {
148159
TurnController: turnController,
149160
TurnAgentFactory: func(thread storage.Thread) (agentimpl.Streamer, error) {
150161
modelID := extractModelID(thread.AgentOptionsJSON)
162+
sessionID := extractSessionID(thread.AgentOptionsJSON)
151163
configOverrides := extractConfigOverrides(thread.AgentOptionsJSON)
152164
switch thread.AgentID {
153165
case "codex":
154166
return codexagent.New(codexagent.Config{
155167
Dir: thread.CWD,
156168
ModelID: modelID,
169+
SessionID: sessionID,
157170
ConfigOverrides: configOverrides,
158171
Name: "codex-embedded",
159172
RuntimeConfig: codexRuntimeConfig,
@@ -162,30 +175,35 @@ func main() {
162175
return opencodeagent.New(opencodeagent.Config{
163176
Dir: thread.CWD,
164177
ModelID: modelID,
178+
SessionID: sessionID,
165179
ConfigOverrides: configOverrides,
166180
})
167181
case "gemini":
168182
return geminiagent.New(geminiagent.Config{
169183
Dir: thread.CWD,
170184
ModelID: modelID,
185+
SessionID: sessionID,
171186
ConfigOverrides: configOverrides,
172187
})
173188
case "kimi":
174189
return kimiagent.New(kimiagent.Config{
175190
Dir: thread.CWD,
176191
ModelID: modelID,
192+
SessionID: sessionID,
177193
ConfigOverrides: configOverrides,
178194
})
179195
case "qwen":
180196
return qwenagent.New(qwenagent.Config{
181197
Dir: thread.CWD,
182198
ModelID: modelID,
199+
SessionID: sessionID,
183200
ConfigOverrides: configOverrides,
184201
})
185202
case "claude":
186203
return claudeagent.New(claudeagent.Config{
187204
Dir: thread.CWD,
188205
ModelID: modelID,
206+
SessionID: sessionID,
189207
ConfigOverrides: configOverrides,
190208
Name: "claude-embedded",
191209
})
@@ -661,6 +679,19 @@ func extractModelID(agentOptionsJSON string) string {
661679
return strings.TrimSpace(opts.ModelID)
662680
}
663681

682+
func extractSessionID(agentOptionsJSON string) string {
683+
var opts struct {
684+
SessionID string `json:"sessionId"`
685+
}
686+
if strings.TrimSpace(agentOptionsJSON) == "" {
687+
return ""
688+
}
689+
if err := json.Unmarshal([]byte(agentOptionsJSON), &opts); err != nil {
690+
return ""
691+
}
692+
return strings.TrimSpace(opts.SessionID)
693+
}
694+
664695
func extractConfigOverrides(agentOptionsJSON string) map[string]string {
665696
var opts struct {
666697
ConfigOverrides map[string]any `json:"configOverrides"`

docs/ACCEPTANCE.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,42 @@ This checklist defines executable acceptance checks for requirements 1-16.
265265
- Verification commands (executed 2026-03-06):
266266
- `go test ./...`
267267
- `cd internal/webui/web && npm run build`
268+
269+
## Requirement 22: ACP Debug Trace Logging
270+
271+
- Operation:
272+
- start server with `--debug=true`.
273+
- execute an ACP-backed request path.
274+
- inspect stderr logs.
275+
- Expected:
276+
- logger runs at debug level.
277+
- stderr includes `acp.message` entries for outbound and inbound ACP JSON-RPC traffic.
278+
- entries include `component`, `direction`, `rpcType`, `method` when present, and sanitized `rpc` payload.
279+
- sensitive fields/tokens are redacted before logging.
280+
- Verification commands (executed 2026-03-11):
281+
- `go test ./internal/observability ./internal/agents/acpstdio -count=1`
282+
- `go test ./cmd/ngent -count=1`
283+
- `cd internal/webui/web && npm run build`
284+
- `go test ./...`
285+
286+
## Requirement 23: ACP Session Sidebar and Resume
287+
288+
- Operation:
289+
- create a thread/agent in the Web UI or API.
290+
- query `GET /v1/threads/{threadId}/sessions` and verify the first page of ACP sessions plus `nextCursor`.
291+
- request the next page through the returned cursor.
292+
- start a turn on a thread without `sessionId` and observe session binding.
293+
- start a follow-up turn on the now-bound thread.
294+
- Expected:
295+
- the backend proxies ACP `session/list` through `GET /v1/threads/{threadId}/sessions`.
296+
- response includes `supported`, `sessions`, and `nextCursor`.
297+
- the Web UI renders a right-side session sidebar with:
298+
- first-page load on active thread selection.
299+
- `Show more` pagination when `nextCursor` is present.
300+
- `New session` action that clears the selected `sessionId`.
301+
- turn SSE emits `session_bound`, and the thread persists `agentOptions.sessionId`.
302+
- once a thread is session-bound, subsequent prompt building no longer injects prior local turns into the provider prompt.
303+
- Verification commands (executed 2026-03-11):
304+
- `go test ./internal/httpapi -run 'TestThreadSessionsListEndpoint|TestTurnSessionBoundPersistsSessionIDAndSkipsContextInjection' -count=1`
305+
- `cd internal/webui/web && npm run build`
306+
- `go test ./...`

docs/API.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ This document defines the current HTTP API contract.
2222
- `duration_ms`
2323
- `resp_bytes`
2424
- Structured log `time` is emitted as UTC `time.DateTime` with second precision.
25+
- When server starts with `--debug=true`, stderr also emits `acp.message` debug entries for ACP JSON-RPC traffic with:
26+
- `component`
27+
- `direction` (`inbound|outbound`)
28+
- `rpcType` (`request|response|notification`)
29+
- `method` when present
30+
- sanitized `rpc` payload with sensitive fields redacted
2531

2632
## Unified Error Envelope
2733

0 commit comments

Comments
 (0)