Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
42fccf7
fix(desktop): fall back to the default macOS update manifest on an em…
elkaix Sep 10, 2026
6933ef9
refactor(agent-core-v2)!: remove the ${now} system prompt template va…
elkaix Sep 10, 2026
9beed04
feat!: graduate remote control, storage and the subagent model pool o…
elkaix Sep 10, 2026
6717a2e
feat(agent-gateway): add a remote-control runtime toggle API
elkaix Sep 10, 2026
07c9293
fix(remote-control): finish the tunnel stack for the runtime toggle
elkaix Sep 10, 2026
17484b0
fix(remote-control): scope the token ban to the relay link and restag…
elkaix Sep 10, 2026
ee29130
fix(minidb): rebuild the search index instead of staying broken
elkaix Sep 10, 2026
2ac8abc
fix(agent-core-v2): keep late task settlement silent after agent tear…
elkaix Sep 10, 2026
cf930e8
chore: order the remote-control devDependency alphabetically
elkaix Sep 10, 2026
e9316a0
fix(cli): restore crash telemetry and honor the disable env var in pr…
elkaix Sep 10, 2026
66d7789
feat(updater): add -y/--yes to skip the upgrade confirmation prompt
elkaix Sep 10, 2026
9882b85
fix(telemetry): deduplicate session_started and model switch events
elkaix Sep 10, 2026
03914fa
refactor(agent-core-v2): remove the staleGuard feature
elkaix Sep 10, 2026
38edafc
fix(goal): remove the time budget cap and stop charging offline time
elkaix Sep 10, 2026
0629108
feat(protocol): preserve media attachment names
elkaix Sep 10, 2026
e75bc55
fix(goal): regenerate the state manifest and pin the v1 close-pause d…
elkaix Sep 10, 2026
de2b335
merge main into fix/reconcile-rows-2026-09-10
elkaix Sep 14, 2026
726f18f
fix: guarantee process shutdown when session telemetry cleanup fails
elkaix Sep 14, 2026
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/pause-goal-clock-on-close.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Exclude time spent with the session closed from goal time budgets.
5 changes: 5 additions & 0 deletions .changeset/preserve-media-attachment-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Preserve image and video filenames in session history.
5 changes: 5 additions & 0 deletions .changeset/print-mode-telemetry-disable-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Fix print mode (`pythinker -p`) ignoring the `PYTHINKER_DISABLE_TELEMETRY` environment variable.
5 changes: 5 additions & 0 deletions .changeset/remove-goal-time-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Remove the 24-hour limit on goal time budgets.
5 changes: 5 additions & 0 deletions .changeset/update-yes-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Add `-y, --yes` to `pythinker upgrade` (alias `pythinker update`) to skip the confirmation prompt and install the update directly.
2 changes: 1 addition & 1 deletion apps/pythinker-code/dist-web/.web-bundle-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"sourceHash": "3856336183464397291f1fe4b79cf621f9251cfe1d579884874392027f2c376c",
"sourceHash": "0670132fd6ebf38fbe6620f3f406ab07bcd253ce8fe8c89e34558d03fd2a4d0f",
"sourceFileCount": 493
}
8 changes: 5 additions & 3 deletions apps/pythinker-code/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { registerWebCommand } from './sub/web';

export type MainCommandHandler = (opts: CLIOptions) => void;
export type PluginNodeRunnerHandler = (entry: string, args: readonly string[]) => void;
export type UpgradeCommandHandler = () => void | Promise<void>;
export type UpgradeCommandHandler = (yes: boolean) => void | Promise<void>;
export type UpdateDownloadHandler = (version: string, manual: boolean) => void;

export function createProgram(
Expand All @@ -27,6 +27,7 @@ export function createProgram(
.description('The Starting Point for Next-Gen Agents')
.version(version, '-V, --version')
.allowUnknownOption(false)
.enablePositionalOptions()
.configureHelp({ helpWidth: 100 })
.helpOption('-h, --help', 'Show help.')
.usage('[options] [command]')
Expand Down Expand Up @@ -125,8 +126,9 @@ export function createProgram(
.command('upgrade')
.alias('update')
.description('Upgrade Pythinker Code to the latest version.')
.action(async () => {
await onUpgrade();
.option('-y, --yes', 'Skip the confirmation prompt and install the update directly.', false)
.action(async (options: { yes?: boolean }) => {
await onUpgrade(options.yes === true);
});

program
Expand Down
6 changes: 4 additions & 2 deletions apps/pythinker-code/src/cli/sub/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface UpgradeDeps {
readonly stdout: WritableLike;
readonly stderr: WritableLike;
readonly isInteractive: boolean;
readonly yes: boolean;
readonly track: UpgradeTrack;
readonly logger: UpgradeLogger;
}
Expand Down Expand Up @@ -87,7 +88,7 @@ export async function handleUpgrade(
const source = await deps.detectInstallSource().catch(() => 'unsupported' as const);
const installCommand = installCommandFor(source, target.version, deps.platform);
const needsConfirmation = source !== 'native';
if (!canAutoInstall(source, deps.platform) || (!deps.isInteractive && needsConfirmation)) {
if (!canAutoInstall(source, deps.platform) || (!deps.yes && !deps.isInteractive && needsConfirmation)) {
trackUpgradeEvent(deps.track, 'upgrade_command_manual_command', {
current_version: currentVersion,
target_version: target.version,
Expand All @@ -102,7 +103,7 @@ export async function handleUpgrade(
return 0;
}

if (deps.isInteractive) {
if (!deps.yes && deps.isInteractive) {
trackUpgradeEvent(deps.track, 'upgrade_command_prompted', {
current_version: currentVersion,
target_version: target.version,
Expand Down Expand Up @@ -185,6 +186,7 @@ function createDefaultUpgradeDeps(overrides: Partial<UpgradeDeps>): UpgradeDeps
stdout: overrides.stdout ?? process.stdout,
stderr: overrides.stderr ?? process.stderr,
isInteractive: overrides.isInteractive ?? (process.stdin.isTTY && process.stdout.isTTY),
yes: overrides.yes ?? false,
track: overrides.track ?? trackTelemetry,
logger: overrides.logger ?? log,
};
Expand Down
48 changes: 40 additions & 8 deletions apps/pythinker-code/src/cli/v2/run-v2-print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ import {
resolveMcpJsonPaths,
} from '@pymodel/agent-core-v2/app/mcpConfig/configLoader';
import { createPythinkerDefaultHeaders, createPythinkerDeviceId } from '@pymodel/pythinker-code-oauth';
import {
initializeTelemetry,
setCrashPhase,
setTelemetryContext,
setTelemetryModel,
shouldEnableTelemetry,
shutdownTelemetry,
} from '@pymodel/pythinker-telemetry';
import type { GoalUpdated } from '@pymodel/agent-core-v2/features/goal/goalOps';
import type { TurnEnded } from '@pymodel/agent-core-v2/agent/loop/turnOps';
import type {
Expand All @@ -85,6 +93,7 @@ import {
CLI_USER_AGENT_PRODUCT,
PROMPT_CLEANUP_TIMEOUT_MS,
} from '#/constant/app';
import { currentPythinkerProfile } from '#/utils/region';

import {
formatGoalSummaryText,
Expand Down Expand Up @@ -174,12 +183,13 @@ export async function runV2Print(
// user left unset are filled, in the memory layer.
await applyPrintModeConfigDefaults(configService);
const defaultModel = configService.get<string>('defaultModel') ?? undefined;
let telemetryEnabled: boolean;
let configTelemetryEnabled: boolean;
try {
telemetryEnabled = configService.get('telemetry') !== false;
configTelemetryEnabled = configService.get('telemetry') !== false;
} catch {
telemetryEnabled = true;
configTelemetryEnabled = true;
}
const telemetryEnabled = shouldEnableTelemetry({ enabled: configTelemetryEnabled });
for (const diagnostic of configService.diagnostics()) {
if (diagnostic.severity === 'warning') {
stderr.write(`Warning: ${diagnostic.message}\n`);
Expand All @@ -193,13 +203,18 @@ export async function runV2Print(
const cleanup = async (): Promise<void> => {
const pending = (cleanupPromise ??= (async () => {
removeTerminationCleanup?.();
setCrashPhase('shutdown');
try {
await restorePermission();
} finally {
if (telemetryService !== undefined) {
await raceWithTimeout(telemetryService.shutdown(), CLI_SHUTDOWN_TIMEOUT_MS);
try {
if (telemetryService !== undefined) {
await raceWithTimeout(telemetryService.shutdown(), CLI_SHUTDOWN_TIMEOUT_MS);
}
} finally {
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }).catch(() => {});
app.dispose();
}
app.dispose();
}
})());
await raceWithTimeout(pending, PROMPT_CLEANUP_TIMEOUT_MS);
Expand All @@ -211,7 +226,10 @@ export async function runV2Print(
// `session_load_failed` fire inside create()/resume(), so an appender wired
// up only after resolveNativeSession() would drop them to the null appender.
// The model below is the best known up front; a resumed session's real
// model is reconciled via setContext once resolved.
// model is reconciled once resolved (v2 via setContext, v1 via
// setTelemetryModel). The v1 pipeline is initialized here too: the
// process-wide crash handlers installed in main() report through its
// default client, so its sink must be attached before the run can crash.
telemetryService = app.accessor.get(ITelemetryService);
if (telemetryEnabled) {
telemetryService.setAppender(
Expand All @@ -222,6 +240,17 @@ export async function runV2Print(
model: opts.model ?? defaultModel,
}),
);
// No `first_launch` on the v1 client: the v2 side already tracks it via
// `telemetryService.track2` below, so tracking here would double-send.
initializeTelemetry({
homeDir,
deviceId,
appName: CLI_USER_AGENT_PRODUCT,
version,
uiMode: PROMPT_UI_MODE,
model: opts.model ?? defaultModel,
endpoint: () => currentPythinkerProfile().telemetryEndpoint,
});
}

try {
Expand All @@ -235,6 +264,9 @@ export async function runV2Print(
restorePermission = resolved.restorePermission;

telemetryService.setContext({ sessionId: resolved.session.id, model: resolved.telemetryModel });
setTelemetryContext({ sessionId: resolved.session.id });
setTelemetryModel(resolved.telemetryModel);
setCrashPhase('runtime');
if (firstLaunch) {
telemetryService.track2('first_launch');
}
Expand Down Expand Up @@ -318,7 +350,7 @@ export function formatTrustGatedMcpWarning(servers: readonly TrustGatedMcpServer
}

function escapeControlChars(value: string): string {
return value.replaceAll(/[\u0000-\u001f\u007f-\u009f]/g, (char) => {
return value.replaceAll(/[\u0000-\u001F\u007F-\u009F]/g, (char) => {
const code = char.codePointAt(0) ?? 0;
return `\\x${code.toString(16).padStart(2, '0')}`;
});
Expand Down
8 changes: 4 additions & 4 deletions apps/pythinker-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export async function handleMainCommand(
return { headlessCompleted: false };
}

export async function handleUpgradeCommand(version: string): Promise<void> {
export async function handleUpgradeCommand(version: string, yes: boolean): Promise<void> {
const telemetryBootstrap = createCliTelemetryBootstrap();
const telemetryClient: TelemetryClient = {
track,
Expand All @@ -118,7 +118,7 @@ export async function handleUpgradeCommand(version: string): Promise<void> {
version,
uiMode: CLI_UI_MODE,
});
exitCode = await handleUpgrade(version, { track, logger: log });
exitCode = await handleUpgrade(version, { track, logger: log, yes });
} finally {
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }).catch(() => {});
await harness.close().catch(() => {});
Expand Down Expand Up @@ -236,8 +236,8 @@ function bootstrap(): void {
process.exit(1);
});
},
() => {
void handleUpgradeCommand(version).catch(async (error: unknown) => {
(yes) => {
void handleUpgradeCommand(version, yes).catch(async (error: unknown) => {
await logStartupFailure('upgrade', error);
process.stderr.write(formatStartupError(error, { operation: 'upgrade' }));
process.stderr.write(`See log: ${resolveGlobalLogPath(resolvePythinkerHome())}\n`);
Expand Down
4 changes: 3 additions & 1 deletion apps/pythinker-code/src/tui/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ export async function handleLoginCommand(host: SlashCommandHost): Promise<void>
? undefined
: { model: selection.model, effort: selection.thinking };
},
refreshConfigAfterLogin: () => host.authFlow.refreshConfigAfterLogin(),
refreshConfigAfterLogin: async () => {
await host.authFlow.refreshConfigAfterLogin();
},
track: (event, properties): void => {
host.track(event, properties);
},
Expand Down
4 changes: 2 additions & 2 deletions apps/pythinker-code/src/tui/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,9 +709,9 @@ export async function applyExperimentalFeatureChanges(
host.refreshSlashCommandAutocomplete();
host.restoreEditor();
if (host.session !== undefined) {
await host.session.reloadSession();
const reloadedSession = await host.harness.reloadSession({ id: host.session.id });
await host.reloadCurrentSessionView(
host.session,
reloadedSession,
'Experimental features updated. Session reloaded.',
);
} else {
Expand Down
23 changes: 20 additions & 3 deletions apps/pythinker-code/src/tui/commands/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,18 +294,35 @@ export async function setDefaultModel(
effort,
model === undefined ? undefined : effectiveModelForHost(host, model),
);
if (host.session === undefined && host.engineV2) {
// A first prompt may still be inside lazy creation: wait it out so the
// pick lands on the new session instead of racing its assembly (same
// coordination as the /model path).
await host.waitForLazyCreation();
}
await host.harness.setConfig({
defaultModel: alias,
thinking,
});
await host.authFlow.refreshConfigAfterLogin();
// Whether activation made the engine emit model_switch (it reached a live
// session AND changed the bound alias — both engines track only an actual
// change). Recorded at activation time rather than snapshotted at entry: a
// lazy session can come live while the config writes above are pending; a
// session created BY activation (v1) or a same-alias rebind does not count
// — both bind the model without an engine event.
let engineTrackedSwitch = await host.authFlow.refreshConfigAfterLogin();
// refreshConfigAfterLogin reactivates from the persisted config, so a pick
// the gate keeps session-only never reaches the runtime — apply it after
// the refresh, or the persisted value would clobber it.
if (thinking.effort === undefined && effort !== 'off' && effort !== 'on') {
await host.authFlow.activateModelAfterLogin(alias, effort);
engineTrackedSwitch =
(await host.authFlow.activateModelAfterLogin(alias, effort)) || engineTrackedSwitch;
}
// When the engine never emitted (no live session, or the alias was already
// bound), the TUI stays the sole producer for the pick.
if (!engineTrackedSwitch) {
host.track('model_switch', { model: alias });
}
host.track('model_switch', { model: alias });
host.showStatus(`Default model set to ${alias} with thinking ${effort}.`);
}

Expand Down
7 changes: 5 additions & 2 deletions apps/pythinker-code/src/tui/commands/reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ export async function handleReloadCommand(host: SlashCommandHost): Promise<void>
const session = host.session;

if (session !== undefined) {
await session.reloadSession({ forcePluginSessionStartReminder: true });
await host.reloadCurrentSessionView(session, 'Session reloaded.');
const reloadedSession = await host.harness.reloadSession({
id: session.id,
forcePluginSessionStartReminder: true,
});
await host.reloadCurrentSessionView(reloadedSession, 'Session reloaded.');
}

const config = await host.harness.getConfig({ reload: true });
Expand Down
Loading
Loading