Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/compaction-max-tokens-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pythoughts/pythinker-code": patch
---

Fix context compaction failing with provider "Invalid max_tokens" errors by capping requested completion tokens to the remaining context window and a safe output ceiling instead of the full context window size.
5 changes: 5 additions & 0 deletions .changeset/dynamic-workflow-progress-stall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pythoughts/pythinker-code": patch
---

Fix Dynamic Workflow progress sticking at 90% during long streaming, show a Finalizing state once all delegated agents finish, and fix member row alignment at narrow widths.
5 changes: 5 additions & 0 deletions .changeset/homebrew-restart-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pythoughts/pythinker-code": minor
---

Prepare verified Homebrew updates in the background and install them automatically on the next interactive launch.
91 changes: 82 additions & 9 deletions apps/pythinker-code/src/cli/sub/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@ import { z } from 'zod';

import { getTuiConfigPath, parseTuiConfig } from '#/tui/config';
import { readUpdateCache } from '#/cli/update/cache';
import { isAutoUpdateDisabledByEnv, shouldAutoInstallUpdates } from '#/cli/update/preflight';
import { readUpdateInstallState } from '#/cli/update/install-state';
import {
automaticUpdateModeFor,
isAutoUpdateDisabledByEnv,
shouldAutoInstallUpdates,
type AutomaticUpdateMode,
} from '#/cli/update/preflight';
import { detectInstallSource } from '#/cli/update/source';
import type { UpdateInstallFailure } from '#/cli/update/types';
import { getHostPackageRoot, getVersion } from '#/cli/version';
import { getUpdateInstallLogFile } from '#/utils/paths';

interface WritableLike {
write(chunk: string): boolean;
Expand Down Expand Up @@ -50,6 +58,12 @@ export interface DoctorRuntimeInfo {
readonly latest: string | null;
readonly checkedAt: string | null;
readonly autoUpdate?: 'on' | 'off' | 'env-disabled';
readonly mode?: AutomaticUpdateMode;
readonly pendingVersion?: string;
readonly pendingRequestedBy?: 'automatic' | 'manual';
readonly activeOperation?: string;
readonly lastFailure?: string;
readonly logPath?: string;
};
}

Expand Down Expand Up @@ -166,11 +180,12 @@ function resolveDeps(deps: Partial<DoctorDeps> | DoctorDeps | undefined): Resolv
runtimeInfo:
deps?.runtimeInfo ??
(async () => {
const [installSource, installations, ripgrep, update, autoInstall] = await Promise.all([
const [installSource, installations, ripgrep, update, installState, autoInstall] = await Promise.all([
detectInstallSource(),
findPythinkerExecutables(),
findExistingRg(resolvePythinkerHome()),
readUpdateCache(),
readUpdateInstallState(),
shouldAutoInstallUpdates(),
]);
return {
Expand All @@ -184,12 +199,31 @@ function resolveDeps(deps: Partial<DoctorDeps> | DoctorDeps | undefined): Resolv
latest: update.latest,
checkedAt: update.checkedAt,
autoUpdate: isAutoUpdateDisabledByEnv() ? 'env-disabled' : autoInstall ? 'on' : 'off',
mode: automaticUpdateModeFor(installSource, process.platform),
pendingVersion: installState.pending?.version,
pendingRequestedBy: installState.pending?.requestedBy,
activeOperation:
installState.active === null
? undefined
: `${installState.active.operation ?? 'install'} ${installState.active.version}`,
lastFailure:
installState.lastFailure === null
? undefined
: formatUpdateFailure(installState.lastFailure),
logPath: getUpdateInstallLogFile(),
},
};
}),
};
}

function formatUpdateFailure(failure: UpdateInstallFailure): string {
const summary = `${failure.operation ?? 'install'} ${failure.version} ` +
`(attempt ${String(failure.attempts)})`;
const message = failure.message?.replaceAll(/\s+/gu, ' ').trim();
return message === undefined || message === '' ? summary : `${summary}: ${message}`;
}

export async function findPythinkerExecutables(
pathValue = process.env['PATH'],
platform: NodeJS.Platform = process.platform,
Expand Down Expand Up @@ -368,25 +402,64 @@ function formatRuntimeInfo(info: DoctorRuntimeInfo | undefined): string[] {
? []
: [
' Update channel: CDN staged rollout',
...(info.update.autoUpdate === undefined
? []
: [
info.update.autoUpdate === 'env-disabled'
? ' Auto-update: disabled by PYTHINKER_CODE_NO_AUTO_UPDATE'
: ` Auto-update: ${info.update.autoUpdate} (tui.toml [upgrade].auto_install)`,
]),
...formatAutomaticUpdate(info),
...(info.update.latest === null
? [' Latest cached version: unavailable']
: [
` Latest cached version: ${info.update.latest}${
info.update.checkedAt === null ? '' : ` (checked ${info.update.checkedAt})`
}`,
]),
...formatPreparedUpdate(info.update),
...(info.update.activeOperation === undefined
? []
: [` Update operation: ${info.update.activeOperation}`]),
...(info.update.lastFailure === undefined
? []
: [` Last update failure: ${info.update.lastFailure}`]),
...(info.update.logPath === undefined ? [] : [` Update log: ${info.update.logPath}`]),
]),
'',
];
}

function formatPreparedUpdate(
update: NonNullable<DoctorRuntimeInfo['update']>,
): string[] {
if (update.pendingVersion === undefined) return [];
if (update.pendingRequestedBy === 'automatic' && update.autoUpdate !== 'on') {
return [
` Prepared update: ${update.pendingVersion} ` +
'(automatic activation paused until auto-update is enabled)',
];
}
return [` Prepared update: ${update.pendingVersion} (installs on next launch)`];
}

function formatAutomaticUpdate(info: DoctorRuntimeInfo): string[] {
const update = info.update;
if (update?.autoUpdate === undefined) return [];
if (update.autoUpdate === 'env-disabled') {
return [
' Auto-update: disabled by PYTHINKER_CODE_NO_AUTO_UPDATE or ' +
'PYTHINKER_CLI_NO_AUTO_UPDATE',
];
}
if (update.autoUpdate === 'off') {
return [' Auto-update: off (tui.toml [upgrade].auto_install)'];
}
switch (update.mode) {
case 'restart-install':
return [' Auto-update: on (prepare in background; install on next launch)'];
case 'background-install':
return [' Auto-update: on (installs in background)'];
case 'manual':
return [` Auto-update: unavailable for ${info.installSource}`];
case undefined:
return [' Auto-update: on (tui.toml [upgrade].auto_install)'];
}
}

function formatResults(results: readonly CheckResult[]): string[] {
const lines: string[] = [];
for (const result of results) {
Expand Down
181 changes: 181 additions & 0 deletions apps/pythinker-code/src/cli/update/activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { gte, valid } from 'semver';

import { getUpdateInstallLogFile } from '#/utils/paths';

import {
activateHomebrewUpdate,
PreparedHomebrewUpdateInvalidError,
} from './homebrew';
import { formatErrorMessage } from './format-error';
import { tryAcquireUpdateInstallLock, type UpdateInstallLockHandle } from './install-lock';
import { readUpdateInstallState, writeUpdateInstallState } from './install-state';
import { detectInstallSource } from './source';
import type { InstallSource, UpdateInstallState, UpdatePreparedHomebrew } from './types';

const ACTIVATION_FAILURE_LIMIT = 2;

export interface ActivatePendingUpdateDeps {
readonly readState: () => Promise<UpdateInstallState>;
readonly writeState: (state: UpdateInstallState) => Promise<void>;
readonly acquireLock: (
request: { readonly version: string },
) => Promise<UpdateInstallLockHandle | null>;
readonly activateHomebrew: (
prepared: UpdatePreparedHomebrew,
) => Promise<{ readonly version: string; readonly executable: string }>;
readonly detectSource: () => Promise<InstallSource>;
readonly now: () => Date;
readonly pid: number;
}

export interface ActivatePendingUpdateOptions {
readonly enabled: boolean;
readonly automaticEnabled: boolean;
readonly deps?: Partial<ActivatePendingUpdateDeps>;
}

function resolveDeps(overrides: Partial<ActivatePendingUpdateDeps> = {}): ActivatePendingUpdateDeps {
return {
readState: overrides.readState ?? (() => readUpdateInstallState()),
writeState: overrides.writeState ?? ((state) => writeUpdateInstallState(state)),
acquireLock: overrides.acquireLock ?? ((request) => tryAcquireUpdateInstallLock(request)),
activateHomebrew:
overrides.activateHomebrew ??
((prepared) => activateHomebrewUpdate(prepared, { logFile: getUpdateInstallLogFile() })),
detectSource: overrides.detectSource ?? (() => detectInstallSource()),
now: overrides.now ?? (() => new Date()),
pid: overrides.pid ?? process.pid,
};
}

function activationAttempts(state: UpdateInstallState, version: string): number {
const failure = state.lastFailure;
return failure?.version === version && failure.operation === 'activate' ? failure.attempts : 0;
}

function isRunningPreparedVersion(currentVersion: string, preparedVersion: string): boolean {
return (
valid(currentVersion) !== null &&
valid(preparedVersion) !== null &&
gte(currentVersion, preparedVersion)
);
}

export async function activatePendingUpdate(
currentVersion: string,
options: ActivatePendingUpdateOptions,
) {
if (!options.enabled) return { status: 'none' as const };
const deps = resolveDeps(options.deps);
let state = await deps.readState();
const pending = state.pending;
if (pending === null) return { status: 'none' as const };
if (pending.requestedBy === 'automatic' && !options.automaticEnabled) {
return { status: 'none' as const };
}

if (await deps.detectSource() !== pending.source) {
await deps.writeState({ ...state, active: null, pending: null });
return { status: 'invalidated' as const, version: pending.version };
}

if (isRunningPreparedVersion(currentVersion, pending.version)) {
const installedAt = deps.now().toISOString();
await deps.writeState({
active: null,
pending: null,
lastFailure: null,
lastSuccess: {
version: currentVersion,
installedAt,
notifiedAt: null,
},
});
return { status: 'finalized' as const, version: currentVersion };
}

if (activationAttempts(state, pending.version) >= ACTIVATION_FAILURE_LIMIT) {
// Terminal: drop the pending record (keeping lastFailure for preflight)
// so later launches stop retrying and reporting an in-progress update.
await deps.writeState({ ...state, pending: null });
return {
status: 'failed' as const,
version: pending.version,
message: `Automatic activation failed ${String(ACTIVATION_FAILURE_LIMIT)} times`,
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const lock = await deps.acquireLock({ version: pending.version });
if (lock === null) return { status: 'in-progress' as const, version: pending.version };

try {
state = await deps.readState();
if (state.pending?.jobId !== pending.jobId) return { status: 'none' as const };
const startedAt = deps.now().toISOString();
const activatingState: UpdateInstallState = {
...state,
active: {
version: pending.version,
source: pending.source,
operation: 'activate',
jobId: pending.jobId,
startedAt,
pid: deps.pid,
},
};
await deps.writeState(activatingState);

try {
const activated = await deps.activateHomebrew(pending);
await deps.writeState({
...activatingState,
active: null,
lastFailure: null,
});
return {
status: 'activated' as const,
version: activated.version,
executable: activated.executable,
};
} catch (error) {
const message = formatErrorMessage(error);
if (error instanceof PreparedHomebrewUpdateInvalidError) {
// Carry the cumulative prepare-failure count so repeated invalid
// artifacts can reach the auto-install failure threshold.
const priorFailure = activatingState.lastFailure;
const prepareAttempts =
priorFailure?.version === pending.version && priorFailure.operation === 'prepare'
? priorFailure.attempts + 1
: 1;
await deps.writeState({
...activatingState,
active: null,
pending: null,
lastFailure: {
version: pending.version,
failedAt: deps.now().toISOString(),
attempts: prepareAttempts,
operation: 'prepare',
message,
},
});
return { status: 'invalidated' as const, version: pending.version };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const attempts = activationAttempts(activatingState, pending.version) + 1;
await deps.writeState({
...activatingState,
active: null,
lastFailure: {
version: pending.version,
failedAt: deps.now().toISOString(),
attempts,
operation: 'activate',
message,
},
});
return { status: 'failed' as const, version: pending.version, message };
}
} finally {
await lock.release().catch(() => {});
}
}
4 changes: 4 additions & 0 deletions apps/pythinker-code/src/cli/update/format-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Shared failure-message formatter for update install/prepare/activate state. */
export function formatErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading