Skip to content

Commit 56fdf95

Browse files
v0.7.1
# v0.7.1 — Stability & Reliability --- ## Improvements - **Pi agent engine 0.56.2** — updated from 0.55.0 with GPT-5.4 support (openai, openai-codex, azure-openai-responses), gpt-5.3-codex fallback for GitHub Copilot, Mistral native conversations integration, and OpenCode Go provider support - **Automation session titles** — automation-initiated sessions now use the automation's name as the session title instead of a truncated prompt snippet; AI title generation is skipped for these sessions (861a5974, 79b9ebb4) - **CLI validate-server** — added `--disable-spinner` / `--no-spinner` flag for CI environments; validate-server auto-bootstraps a temp workspace and LLM connection when none exist (84b3378c, 1ea478d7) ## Bug Fixes - **Branch creation reliability** — fixed a race condition where `ERR_STREAM_WRITE_AFTER_END` from the SDK's `ProcessTransport` left branch creation in an infinite loading state; added 15s timeout with interrupt-based cancellation for hung preflight queries (8ff37102, 0dc1d2e6) - **Base64 false positives** — replaced heuristic base64 detection with a strict canonicalization pipeline (charset regex, normalize, auto-pad, roundtrip verify) to eliminate false positives on English text and other non-base64 content. Fixes [#344](#344) - **Cross-machine session recovery** — persist cleared `sdkSessionId` on session expiry recovery, preventing repeated stale-ID failures when syncing sessions across machines. Fixes [#342](#342) - **Spawn ENOENT during auto-update** — detect app bundle swap during auto-update using structured error fields + regex fallback, with bounded single retry. Fixes [#268](#268) - **Duplicate LLM connections on re-auth** — fixed stale React closure in `handleReauthenticateConnection` that generated new slugs (e.g. `chatgpt-plus-2`) instead of reusing existing ones; added `updateOnly` guard on the server side (b735f9df) - **Codex token expiry** — Pi/Codex auth-expiry errors now route through the typed_error auth-retry pipeline instead of appearing as red error messages; global refresh mutex prevents cross-session token refresh races (1d85bc87) - **Settings "Check Now" stuck at 0%** — unified `autoDownload` policy between Settings check and native menu so download-progress events arrive correctly (b8ad8c44)
1 parent 24ab785 commit 56fdf95

File tree

39 files changed

+985
-232
lines changed

39 files changed

+985
-232
lines changed

apps/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@craft-agent/cli",
3-
"version": "0.7.0",
3+
"version": "0.7.1",
44
"description": "Terminal client for Craft Agent server",
55
"type": "module",
66
"main": "src/index.ts",

apps/electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@craft-agent/electron",
3-
"version": "0.7.0",
3+
"version": "0.7.1",
44
"description": "Electron desktop app for Craft Agents",
55
"main": "dist/main.cjs",
66
"private": true,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# v0.7.1 — Stability & Reliability
2+
3+
---
4+
5+
## Improvements
6+
7+
- **Pi agent engine 0.56.2** — updated from 0.55.0 with GPT-5.4 support (openai, openai-codex, azure-openai-responses), gpt-5.3-codex fallback for GitHub Copilot, Mistral native conversations integration, and OpenCode Go provider support
8+
- **Automation session titles** — automation-initiated sessions now use the automation's name as the session title instead of a truncated prompt snippet; AI title generation is skipped for these sessions (861a5974, 79b9ebb4)
9+
- **CLI validate-server** — added `--disable-spinner` / `--no-spinner` flag for CI environments; validate-server auto-bootstraps a temp workspace and LLM connection when none exist (84b3378c, 1ea478d7)
10+
11+
## Bug Fixes
12+
13+
- **Branch creation reliability** — fixed a race condition where `ERR_STREAM_WRITE_AFTER_END` from the SDK's `ProcessTransport` left branch creation in an infinite loading state; added 15s timeout with interrupt-based cancellation for hung preflight queries (8ff37102, 0dc1d2e6)
14+
- **Base64 false positives** — replaced heuristic base64 detection with a strict canonicalization pipeline (charset regex, normalize, auto-pad, roundtrip verify) to eliminate false positives on English text and other non-base64 content. Fixes [#344](https://github.com/lukilabs/craft-agents-oss/issues/344)
15+
- **Cross-machine session recovery** — persist cleared `sdkSessionId` on session expiry recovery, preventing repeated stale-ID failures when syncing sessions across machines. Fixes [#342](https://github.com/lukilabs/craft-agents-oss/issues/342)
16+
- **Spawn ENOENT during auto-update** — detect app bundle swap during auto-update using structured error fields + regex fallback, with bounded single retry. Fixes [#268](https://github.com/lukilabs/craft-agents-oss/issues/268)
17+
- **Duplicate LLM connections on re-auth** — fixed stale React closure in `handleReauthenticateConnection` that generated new slugs (e.g. `chatgpt-plus-2`) instead of reusing existing ones; added `updateOnly` guard on the server side (b735f9df)
18+
- **Codex token expiry** — Pi/Codex auth-expiry errors now route through the typed_error auth-retry pipeline instead of appearing as red error messages; global refresh mutex prevents cross-session token refresh races (1d85bc87)
19+
- **Settings "Check Now" stuck at 0%** — unified `autoDownload` policy between Settings check and native menu so download-progress events arrive correctly (b8ad8c44)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Tests for the updateOnly guard in SETUP_LLM_CONNECTION.
3+
*
4+
* The updateOnly flag prevents accidental connection creation during
5+
* re-authentication flows. When set, the handler must reject if the
6+
* slug doesn't map to an existing connection.
7+
*
8+
* Since the handler is tightly coupled to the RPC server, we test the
9+
* guard logic by mocking the config/credential layer and invoking
10+
* the decision path directly.
11+
*/
12+
import { describe, it, expect, mock, beforeEach } from 'bun:test'
13+
import { createBuiltInConnection } from '@craft-agent/server-core/domain'
14+
import type { LlmConnectionSetup } from '@craft-agent/shared/protocol'
15+
16+
// ============================================================
17+
// Simulated updateOnly guard logic
18+
// (mirrors the handler code in llm-connections.ts)
19+
// ============================================================
20+
21+
interface MockDeps {
22+
getLlmConnection: (slug: string) => unknown | null
23+
deleteLlmCredentials: (slug: string) => Promise<void>
24+
createBuiltInConnection: (slug: string, baseUrl?: string) => unknown
25+
addLlmConnection: (conn: unknown) => boolean
26+
}
27+
28+
/**
29+
* Extracted guard logic from SETUP_LLM_CONNECTION handler.
30+
* Returns { action, error? } indicating what the handler would do.
31+
*/
32+
function evaluateSetupGuard(
33+
setup: Pick<LlmConnectionSetup, 'slug' | 'updateOnly'>,
34+
deps: MockDeps,
35+
): { action: 'update' | 'create' | 'reject'; error?: string } {
36+
const connection = deps.getLlmConnection(setup.slug)
37+
if (connection) {
38+
return { action: 'update' }
39+
}
40+
if (setup.updateOnly) {
41+
// Handler would clean up orphaned credentials and reject
42+
deps.deleteLlmCredentials(setup.slug).catch(() => {})
43+
return { action: 'reject', error: 'Connection not found. Cannot re-authenticate a non-existent connection.' }
44+
}
45+
return { action: 'create' }
46+
}
47+
48+
// ============================================================
49+
// Tests
50+
// ============================================================
51+
52+
describe('SETUP_LLM_CONNECTION updateOnly guard', () => {
53+
let mockDeleteCreds: ReturnType<typeof mock>
54+
55+
beforeEach(() => {
56+
mockDeleteCreds = mock(() => Promise.resolve())
57+
})
58+
59+
it('updateOnly=true + missing slug → rejects', () => {
60+
const result = evaluateSetupGuard(
61+
{ slug: 'nonexistent', updateOnly: true },
62+
{
63+
getLlmConnection: () => null,
64+
deleteLlmCredentials: mockDeleteCreds as any,
65+
createBuiltInConnection,
66+
addLlmConnection: () => true,
67+
},
68+
)
69+
expect(result.action).toBe('reject')
70+
expect(result.error).toContain('Connection not found')
71+
})
72+
73+
it('updateOnly=true + missing slug → cleans up orphaned credentials', () => {
74+
evaluateSetupGuard(
75+
{ slug: 'chatgpt-plus-2', updateOnly: true },
76+
{
77+
getLlmConnection: () => null,
78+
deleteLlmCredentials: mockDeleteCreds as any,
79+
createBuiltInConnection,
80+
addLlmConnection: () => true,
81+
},
82+
)
83+
expect(mockDeleteCreds).toHaveBeenCalledWith('chatgpt-plus-2')
84+
})
85+
86+
it('updateOnly=true + existing slug → updates normally', () => {
87+
const result = evaluateSetupGuard(
88+
{ slug: 'chatgpt-plus', updateOnly: true },
89+
{
90+
getLlmConnection: () => ({ slug: 'chatgpt-plus', name: 'ChatGPT Plus' }),
91+
deleteLlmCredentials: mockDeleteCreds as any,
92+
createBuiltInConnection,
93+
addLlmConnection: () => true,
94+
},
95+
)
96+
expect(result.action).toBe('update')
97+
expect(mockDeleteCreds).not.toHaveBeenCalled()
98+
})
99+
100+
it('default flow (no updateOnly) + missing slug → creates', () => {
101+
const result = evaluateSetupGuard(
102+
{ slug: 'chatgpt-plus-2' },
103+
{
104+
getLlmConnection: () => null,
105+
deleteLlmCredentials: mockDeleteCreds as any,
106+
createBuiltInConnection,
107+
addLlmConnection: () => true,
108+
},
109+
)
110+
expect(result.action).toBe('create')
111+
expect(mockDeleteCreds).not.toHaveBeenCalled()
112+
})
113+
114+
it('default flow + existing slug → updates', () => {
115+
const result = evaluateSetupGuard(
116+
{ slug: 'chatgpt-plus' },
117+
{
118+
getLlmConnection: () => ({ slug: 'chatgpt-plus' }),
119+
deleteLlmCredentials: mockDeleteCreds as any,
120+
createBuiltInConnection,
121+
addLlmConnection: () => true,
122+
},
123+
)
124+
expect(result.action).toBe('update')
125+
})
126+
})

apps/electron/src/main/handlers/system.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ export function registerSystemGuiHandlers(server: RpcServer, deps: HandlerDeps):
267267
// Auto-update handlers
268268
server.handle(RPC_CHANNELS.update.CHECK, async () => {
269269
const { checkForUpdates } = await import('../auto-update')
270-
return checkForUpdates({ autoDownload: false })
270+
return checkForUpdates({ autoDownload: true })
271271
})
272272

273273
server.handle(RPC_CHANNELS.update.GET_INFO, async () => {

apps/electron/src/renderer/hooks/__tests__/useOnboarding.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,51 @@ describe('apiSetupMethodToConnectionSetup', () => {
121121
expect(setup.slug).toBe('claude-max-2')
122122
})
123123
})
124+
125+
// ============================================================
126+
// Reauth slug regression tests
127+
// ============================================================
128+
129+
describe('reauth slug resolution', () => {
130+
it('slug override wins over null editingSlug (stale closure scenario)', () => {
131+
// Simulates the reauth bug: editingSlug is null (stale closure),
132+
// but connectionSlugOverride provides the correct slug.
133+
const existingSlugs = new Set(['chatgpt-plus'])
134+
135+
// Without override: generates -2 (the bug)
136+
const wrongSlug = resolveSlugForMethod('pi_chatgpt_oauth', null, existingSlugs)
137+
expect(wrongSlug).toBe('chatgpt-plus-2')
138+
139+
// With override: reuses existing slug (the fix)
140+
const correctSlug = resolveSlugForMethod('pi_chatgpt_oauth', 'chatgpt-plus', existingSlugs)
141+
expect(correctSlug).toBe('chatgpt-plus')
142+
})
143+
144+
it('apiSetupMethodToConnectionSetup uses override slug for reauth', () => {
145+
const existingSlugs = new Set(['chatgpt-plus'])
146+
const setup = apiSetupMethodToConnectionSetup(
147+
'pi_chatgpt_oauth',
148+
{},
149+
'chatgpt-plus', // override slug (reauth)
150+
existingSlugs,
151+
)
152+
expect(setup.slug).toBe('chatgpt-plus')
153+
})
154+
155+
it('new connection flow still generates unique slugs when base is taken', () => {
156+
const existingSlugs = new Set(['chatgpt-plus'])
157+
const setup = apiSetupMethodToConnectionSetup(
158+
'pi_chatgpt_oauth',
159+
{},
160+
null, // no editing slug (new connection)
161+
existingSlugs,
162+
)
163+
expect(setup.slug).toBe('chatgpt-plus-2')
164+
})
165+
166+
it('copilot reauth uses override slug', () => {
167+
const existingSlugs = new Set(['github-copilot'])
168+
const slug = resolveSlugForMethod('pi_copilot_oauth', 'github-copilot', existingSlugs)
169+
expect(slug).toBe('github-copilot')
170+
})
171+
})

apps/electron/src/renderer/hooks/useAutomations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export function useAutomations(
9595
window.electronAPI.testAutomation({
9696
workspaceId: activeWorkspaceId,
9797
automationId: automation.id,
98+
automationName: automation.name,
9899
actions: automation.actions,
99100
permissionMode: automation.permissionMode,
100101
labels: automation.labels,

apps/electron/src/renderer/hooks/useOnboarding.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ interface UseOnboardingReturn {
5959

6060
// Local model
6161
handleSubmitLocalModel: (data: LocalModelSubmitData) => void
62-
handleStartOAuth: (methodOverride?: ApiSetupMethod) => void
62+
handleStartOAuth: (methodOverride?: ApiSetupMethod, connectionSlugOverride?: string) => void
6363

6464
// Claude OAuth (two-step flow)
6565
isWaitingForCode: boolean
@@ -224,6 +224,8 @@ export function useOnboarding({
224224
modelSelectionMode?: 'automaticallySyncedFromProvider' | 'userDefined3Tier'
225225
},
226226
methodOverride?: ApiSetupMethod,
227+
connectionSlugOverride?: string,
228+
updateOnly?: boolean,
227229
): Promise<boolean> => {
228230
const method = methodOverride ?? state.apiSetupMethod
229231
if (!method) {
@@ -241,9 +243,11 @@ export function useOnboarding({
241243
models: options?.models,
242244
piAuthProvider: options?.piAuthProvider,
243245
modelSelectionMode: options?.modelSelectionMode,
244-
}, editingSlug, existingSlugs)
246+
}, connectionSlugOverride ?? editingSlug, existingSlugs)
245247
// Use new unified API
246-
const result = await window.electronAPI.setupLlmConnection(setup)
248+
const result = await window.electronAPI.setupLlmConnection(
249+
updateOnly ? { ...setup, updateOnly: true } : setup
250+
)
247251

248252
if (result.success) {
249253
setState(s => ({ ...s, completionStatus: 'complete' }))
@@ -438,8 +442,8 @@ export function useOnboarding({
438442
// `method` is passed explicitly to break the stale-closure chain — the OAuth
439443
// await crosses renders, so handleSaveConfig's closure may have an outdated
440444
// state.apiSetupMethod.
441-
const saveAndValidateConnection = useCallback(async (connectionSlug: string, method: ApiSetupMethod, credential?: string): Promise<boolean> => {
442-
const saved = await handleSaveConfig(credential, undefined, method)
445+
const saveAndValidateConnection = useCallback(async (connectionSlug: string, method: ApiSetupMethod, credential?: string, updateOnly?: boolean): Promise<boolean> => {
446+
const saved = await handleSaveConfig(credential, undefined, method, connectionSlug, updateOnly)
443447
if (!saved) {
444448
setState(s => ({ ...s, credentialStatus: 'error' }))
445449
return false
@@ -461,7 +465,7 @@ export function useOnboarding({
461465
const [copilotDeviceCode, setCopilotDeviceCode] = useState<{ userCode: string; verificationUri: string } | undefined>()
462466

463467
// Start OAuth flow (Claude or ChatGPT depending on selected method)
464-
const handleStartOAuth = useCallback(async (methodOverride?: ApiSetupMethod) => {
468+
const handleStartOAuth = useCallback(async (methodOverride?: ApiSetupMethod, connectionSlugOverride?: string) => {
465469
const effectiveMethod = methodOverride ?? state.apiSetupMethod
466470

467471
if (methodOverride && methodOverride !== state.apiSetupMethod) {
@@ -488,11 +492,13 @@ export function useOnboarding({
488492
try {
489493
// ChatGPT OAuth (single-step flow - opens browser, captures tokens automatically)
490494
if (effectiveMethod === 'pi_chatgpt_oauth') {
491-
const connectionSlug = apiSetupMethodToConnectionSetup(effectiveMethod, {}, editingSlug, existingSlugs).slug
495+
const effectiveEditingSlug = connectionSlugOverride ?? editingSlug
496+
const isReauth = !!effectiveEditingSlug
497+
const connectionSlug = apiSetupMethodToConnectionSetup(effectiveMethod, {}, effectiveEditingSlug, existingSlugs).slug
492498
const result = await window.electronAPI.startChatGptOAuth(connectionSlug)
493499

494500
if (result.success) {
495-
await saveAndValidateConnection(connectionSlug, effectiveMethod)
501+
await saveAndValidateConnection(connectionSlug, effectiveMethod, undefined, isReauth)
496502
} else {
497503
setState(s => ({
498504
...s,
@@ -505,7 +511,9 @@ export function useOnboarding({
505511

506512
// Copilot OAuth (device flow — polls for token after user enters code on GitHub)
507513
if (effectiveMethod === 'pi_copilot_oauth') {
508-
const connectionSlug = apiSetupMethodToConnectionSetup(effectiveMethod, {}, editingSlug, existingSlugs).slug
514+
const effectiveEditingSlug = connectionSlugOverride ?? editingSlug
515+
const isReauth = !!effectiveEditingSlug
516+
const connectionSlug = apiSetupMethodToConnectionSetup(effectiveMethod, {}, effectiveEditingSlug, existingSlugs).slug
509517

510518
// Subscribe to device code event before starting the flow
511519
const cleanup = window.electronAPI.onCopilotDeviceCode((data) => {
@@ -516,7 +524,7 @@ export function useOnboarding({
516524
const result = await window.electronAPI.startCopilotOAuth(connectionSlug)
517525

518526
if (result.success) {
519-
await saveAndValidateConnection(connectionSlug, effectiveMethod)
527+
await saveAndValidateConnection(connectionSlug, effectiveMethod, undefined, isReauth)
520528
} else {
521529
setState(s => ({
522530
...s,
@@ -614,7 +622,7 @@ export function useOnboarding({
614622

615623
if (result.success && result.token) {
616624
setIsWaitingForCode(false)
617-
await saveAndValidateConnection(connectionSlug, 'claude_oauth', result.token)
625+
await saveAndValidateConnection(connectionSlug, 'claude_oauth', result.token, !!editingSlug)
618626
} else {
619627
setState(s => ({
620628
...s,

apps/electron/src/renderer/pages/settings/AiSettingsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ export default function AiSettingsPage() {
686686
const method = connection.providerType === 'pi'
687687
? (connection.piAuthProvider === 'github-copilot' ? 'pi_copilot_oauth' : 'pi_chatgpt_oauth')
688688
: 'claude_oauth'
689-
apiSetupOnboarding.handleStartOAuth(method)
689+
apiSetupOnboarding.handleStartOAuth(method, connection.slug)
690690
}
691691
}, [apiSetupOnboarding, openApiSetup])
692692

apps/viewer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@craft-agent/viewer",
3-
"version": "0.7.0",
3+
"version": "0.7.1",
44
"description": "Web viewer for Craft Agents sessions - upload and share session transcripts",
55
"private": true,
66
"type": "module",

0 commit comments

Comments
 (0)