Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@

### Changed

- Distinguished Goals that are armed and waiting for their first Turn from
Goals that are already running across Desktop and CLI/TUI surfaces. Goal
conditions shown in client status, tooltip, lineage, and reconciliation
displays are now redacted. The Runtime Host compatibility epoch moves to 111.
- Made typed `request()` the sole direct Runtime Host operation API; removed the 17 forwarding
aliases from direct and reconnecting connections while preserving status validation,
subscriptions, capabilities, listeners, lifecycle, and close behavior.
Expand Down
23 changes: 23 additions & 0 deletions apps/desktop/src/main/__tests__/goal-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,29 @@ describe('useGoalController', () => {
assert.equal(pauseCalls, 2);
});

it('shows an armed marker only while the first Turn is unbound', async () => {
const { root } = installReactRenderer();
const defaults = createFakeGoalServices();
const services = createFakeGoalServices({
goal: {
...defaults.goal,
get: async () => ({ ...goal('a'), armedAt: 150 }),
},
});

await act(async () => renderController(root, services, input('a')));
assert.equal(controller().selectors.indicator?.isArmed, true);

const boundServices = createFakeGoalServices({
goal: {
...defaults.goal,
get: async () => ({ ...goal('a'), armedAt: 150, boundTurnId: 'turn-1' }),
},
});
await act(async () => renderController(root, boundServices, input('a')));
assert.equal(controller().selectors.indicator?.isArmed, false);
});

it('routes resume and clear controls for paused Goals', async () => {
const { root } = installReactRenderer();
const calls: string[] = [];
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/main/__tests__/goal-dialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,21 @@ test('closes only for armed and locks reconciled state until reopen', async () =
assert.equal(harness.closed, 1);
});

test('redacts secrets in a reconciled Goal condition', async () => {
const secret = 'sk-ant-api03-abc123def456ghi789jkl0mn1opq';
const harness = installGoalDialog(async () => ({
kind: 'reconciled',
currentGoal: { ...goalState(), condition: `Use Authorization: Bearer ${secret}` },
matchesRequestedState: true,
}));
await harness.render('session-1');
await setInputValue(harness.document, 'textarea', 'Finish session one');
await clickButton(harness.document, 'Start');

assert.equal(harness.document.body.textContent.includes(secret), false);
assert.match(harness.document.body.textContent, /Authorization: Bearer <redacted>/);
});

test('keeps the Goal form editable after a deterministic rejection', async () => {
const harness = installGoalDialog(async () => {
throw new Error('Goal already exists');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,8 @@ function goalProjection(revision: number) {
lastReason: null,
achievedAt: null,
pausedAt: null,
armedAt: null,
boundTurnId: null,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ test('goal:arm reconciles a lost dispatched response without dispatching again',
tokensAtStart: 0,
tokensNow: 120,
tokensBaselinePending: false,
armedAt: 7,
},
matchesRequestedState: true,
},
Expand Down Expand Up @@ -311,6 +312,7 @@ test('goal:arm reconciliation reports different, missing, and unavailable author
tokensAtStart: 0,
tokensNow: 120,
tokensBaselinePending: false,
armedAt: 7,
},
matchesRequestedState: false,
},
Expand Down Expand Up @@ -490,6 +492,7 @@ test('adapts Host Goal, Task, Deep Research, and Resource projections', async ()
tokensAtStart: 0,
tokensNow: 120,
tokensBaselinePending: false,
armedAt: 7,
});
await ipc.invoke('goal:clear', 'session-1');
await ipc.invoke('goal:pause', 'session-1');
Expand Down Expand Up @@ -1217,6 +1220,8 @@ function baseGoalProjection() {
lastReason: null,
achievedAt: null,
pausedAt: null,
armedAt: 7,
boundTurnId: null,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2858,6 +2858,8 @@ test("publishes Host sidecar and graph invalidations without inventing Session s
lastReason: null,
achievedAt: null,
pausedAt: null,
armedAt: null,
boundTurnId: null,
},
}),
});
Expand Down Expand Up @@ -2966,6 +2968,8 @@ function activeGoal() {
lastReason: null,
achievedAt: null,
pausedAt: null,
armedAt: null,
boundTurnId: null,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {
AgentGraphClientSnapshotOptions,
AgentGraphOperatorInspection,
} from '@maka/runtime/stream-graph-read-model';
import { DEFAULT_MAX_ITERATIONS, type GoalState } from '@maka/runtime/goal-state';
import { DEFAULT_MAX_ITERATIONS } from '@maka/runtime/goal-state';
import type { ShellRunPtyDataEvent } from '@maka/runtime/shell-run-contract';
import type {
GoalProjection,
Expand All @@ -37,6 +37,7 @@ import {
GOAL_ARM_REQUEST_KEYS,
type GoalArmOutcome,
} from '../shared/goal-arm.js';
import type { DesktopGoalState } from '../shared/goal-arm.js';
import { projectHostedDeepResearch } from './deep-research-desktop-projection.js';
import {
handleReconciledControl,
Expand Down Expand Up @@ -451,7 +452,7 @@ function optionalCount(value: unknown, label: string): number | null {
return value;
}

function toDesktopGoal(goal: GoalProjection): GoalState {
function toDesktopGoal(goal: GoalProjection): DesktopGoalState {
return {
id: goal.goalId,
revision: goal.revision,
Expand All @@ -470,6 +471,8 @@ function toDesktopGoal(goal: GoalProjection): GoalState {
...(goal.lastReason === null ? {} : { lastReason: goal.lastReason }),
...(goal.achievedAt === null ? {} : { achievedAt: goal.achievedAt }),
...(goal.pausedAt === null ? {} : { pausedAt: goal.pausedAt }),
...(goal.armedAt === null ? {} : { armedAt: goal.armedAt }),
...(goal.boundTurnId === null ? {} : { boundTurnId: goal.boundTurnId }),
};
}

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1372,7 +1372,7 @@ export interface MakaBridge {
};
goal: {
/** The session's current goal (null when none is set). */
get(sessionId: string): Promise<import('@maka/runtime/goal-state').GoalState | null>;
get(sessionId: string): Promise<import('../shared/goal-arm').DesktopGoalState | null>;
/**
* Arm a goal for this session. It drives the session from the next turn
* on; arming alone starts nothing. Rejects when the session already has an
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ import type {
} from '@maka/runtime/stream-graph-read-model';
import type { BotStatus, WechatBridgeQrCodeResult } from '@maka/runtime/bots';
import type { ShellRunPtyDataEvent, ShellRunPtySnapshot } from '@maka/runtime/shell-run-contract';
import type { GoalState } from '@maka/runtime/goal-state';
import type { DesktopGoalState } from '../shared/goal-arm.js';
import type { BundledSkillCatalogEntry, ManagedSkillSourceEntry, ManagedSkillUpdatePreview, SkillEntry } from '@maka/ui';
import type { ConfigCategory } from '@maka/storage/config-transfer';
import {
Expand Down Expand Up @@ -2752,7 +2752,7 @@ const makaBridge = {
},
},
goal: {
get(sessionId: string): Promise<GoalState | null> {
get(sessionId: string): Promise<DesktopGoalState | null> {
return invokeProjectedSessionRuntimeHost('goal:get', sessionId);
},
arm(sessionId: string, goal: GoalArmRequest): Promise<GoalArmOutcome> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
useState,
type ComponentProps,
} from 'react';
import { isGoalArmedAwaitingFirstTurn } from '@maka/core/goal';
import { useUiLocale, type ChatView } from '@maka/ui';
import {
getShellCopy,
Expand Down Expand Up @@ -144,6 +145,7 @@ export function useGoalController(
iterations: activeGoal.iterations,
maxIterations: activeGoal.maxIterations,
setAt: activeGoal.setAt,
isArmed: isGoalArmedAwaitingFirstTurn(activeGoal),
tokensSpent: activeGoal.tokensNow,
...(activeGoal.tokenBudget !== undefined
? { tokenBudget: activeGoal.tokenBudget }
Expand Down
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/features/goals/model/live-goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,22 @@
* under the License.
*/

import type { GoalState, GoalStatus } from '@maka/core/goal';
import type { GoalStatus } from '@maka/core/goal';
import type { DesktopGoalState } from '../../../../shared/goal-arm.js';

type LiveGoalStatus = Extract<GoalStatus, 'active' | 'waiting'>;

export type LiveGoalState =
| (GoalState & { readonly status: LiveGoalStatus })
| (GoalState & { readonly status: 'paused'; readonly pausedAt: number });
| (DesktopGoalState & { readonly status: LiveGoalStatus })
| (DesktopGoalState & { readonly status: 'paused'; readonly pausedAt: number });

const LIVE_GOAL_STATUSES: ReadonlySet<GoalStatus> = new Set([
'active',
'waiting',
'paused',
]);

export function isLiveGoal(goal: GoalState): goal is LiveGoalState {
export function isLiveGoal(goal: DesktopGoalState): goal is LiveGoalState {
return (
LIVE_GOAL_STATUSES.has(goal.status) &&
(goal.status !== 'paused' ||
Expand Down
5 changes: 2 additions & 3 deletions apps/desktop/src/renderer/features/goals/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
* under the License.
*/

import type { GoalState } from '@maka/core/goal';
import type { GoalArmOutcome } from '../../../shared/goal-arm.js';
import type { DesktopGoalState, GoalArmOutcome } from '../../../shared/goal-arm.js';

export type { GoalArmOutcome } from '../../../shared/goal-arm.js';

Expand All @@ -32,7 +31,7 @@ export interface GoalArmInput {

/** The minimum environment capability needed by the Goals feature. */
export interface GoalService {
get(sessionId: string): Promise<GoalState | null>;
get(sessionId: string): Promise<DesktopGoalState | null>;
arm(sessionId: string, goal: GoalArmInput): Promise<GoalArmOutcome>;
clear(sessionId: string): Promise<void>;
pause(sessionId: string): Promise<void>;
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/renderer/features/goals/ui/goal-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
GOAL_MAX_ITERATIONS_LIMIT,
GOAL_TOKEN_BUDGET_MINIMUM,
} from '@maka/core/goal';
import { useUiLocale } from '@maka/ui';
import { redactSecrets, useUiLocale } from '@maka/ui';
import {
getShellCopy,
localizedShellErrorMessage,
Expand Down Expand Up @@ -109,12 +109,12 @@ export function GoalDialog(props: GoalDialogProps) {
switch (reconciliation.kind) {
case 'matching_goal':
return copy.reconciledMatching(
reconciliation.goal.condition,
redactSecrets(reconciliation.goal.condition),
copy.statusLabels[reconciliation.goal.status],
);
case 'different_goal':
return copy.reconciledDifferent(
reconciliation.goal.condition,
redactSecrets(reconciliation.goal.condition),
copy.statusLabels[reconciliation.goal.status],
);
case 'no_goal':
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/src/shared/goal-arm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@

import type { GoalState } from '@maka/runtime/goal-state';

/** Desktop-only runtime detail; it is transient and never persisted with a Goal. */
export type DesktopGoalState = GoalState & {
readonly boundTurnId?: string;
};

/**
* What the renderer sends to arm a Goal.
*
Expand All @@ -34,10 +39,10 @@ export interface GoalArmRequest {
}

export type GoalArmOutcome =
| { readonly kind: 'armed'; readonly goal: GoalState }
| { readonly kind: 'armed'; readonly goal: DesktopGoalState }
| {
readonly kind: 'reconciled';
readonly currentGoal: GoalState | null;
readonly currentGoal: DesktopGoalState | null;
readonly matchesRequestedState: boolean;
}
| { readonly kind: 'reconciliation_unavailable' };
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/stories/app-shell.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,7 @@ export const SessionContextLayer: Story = {
goal={{
condition: '把 Session Context Layer 收敛到可 review 状态',
status: 'active',
isArmed: false,
iterations: 4,
maxIterations: 12,
setAt: Date.now() - 12 * 60_000,
Expand All @@ -1287,6 +1288,7 @@ export const SessionContextLayerWaiting: Story = {
goal={{
condition: '等待 CI 状态变化后继续处理 review',
status: 'waiting',
isArmed: false,
iterations: 4,
maxIterations: 12,
setAt: Date.now() - 12 * 60_000,
Expand All @@ -1307,6 +1309,7 @@ export const SessionContextLayerPaused: Story = {
goal={{
condition: '把 Session Context Layer 收敛到可 review 状态',
status: 'paused',
isArmed: false,
iterations: 4,
maxIterations: 12,
setAt: pausedAt - 8 * 60_000,
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/__tests__/pi-goal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ function goal(overrides: Partial<GoalProjection> = {}): GoalProjection {
lastReason: null,
achievedAt: null,
pausedAt: null,
armedAt: null,
boundTurnId: null,
...overrides,
};
}
Expand Down Expand Up @@ -99,6 +101,25 @@ describe('pi-goal display helpers', () => {
);
});

test('armed Goals remain set until their first bound Turn, without a running notice or elapsed time', () => {
const armed = goal({ armedAt: 1_000 });
assert.equal(goalStatusLineText(armed, 61_000), 'goal set 3/50');
assert.deepEqual(goalSummaryLines(armed, 61_000).slice(0, 2), [
'Goal: Ship the feature',
'Status: set · 3/50 iterations',
]);
assert.equal(
goalAttachedNoticeText(armed),
'Autonomous goal is set (3/50): Ship the feature — it takes hold on the next Turn.',
);
});

test('a bound first Turn makes the same Goal running again', () => {
const running = goal({ armedAt: 1_000, boundTurnId: 'turn-1' });
assert.equal(goalStatusLineText(running, 61_000), 'goal 3/50 1m');
assert.match(goalAttachedNoticeText(running), /Autonomous goal is running/);
});

test('summary lines include budget only when set and the evaluator note only when present', () => {
const plain = goalSummaryLines(goal(), 61_000);
assert.equal(plain.length, 2);
Expand Down Expand Up @@ -184,4 +205,14 @@ describe('pi-goal display helpers', () => {
const long = goalAttachedNoticeText(goal({ condition: 'x'.repeat(200) }));
assert.ok(long.includes('…') && long.length <= 210);
});

test('redacts secrets from condition text in CLI goal displays', () => {
const secret = 'sk-ant-api03-abc123def456ghi789jkl0mn1opq';
const current = goal({ condition: `Use Authorization: Bearer ${secret}` });

for (const text of [goalAttachedNoticeText(current), goalSummaryLines(current, 61_000)[0]!]) {
assert.equal(text.includes(secret), false);
assert.match(text, /<redacted>/);
}
});
});
6 changes: 6 additions & 0 deletions packages/cli/src/__tests__/pi-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ describe('Maka Pi TUI transcript', () => {
lastReason: null,
achievedAt: null,
pausedAt: null,
armedAt: null,
boundTurnId: null,
} as const;
const active = stripAnsi(
renderMakaPiStatusLine({ ...meta(), goal: { ...base, status: 'active' as const } }, 120),
Expand Down Expand Up @@ -416,6 +418,8 @@ describe('Maka Pi TUI transcript', () => {
lastReason: null,
achievedAt: null,
pausedAt: null,
armedAt: null,
boundTurnId: null,
status: 'active' as const,
},
},
Expand Down Expand Up @@ -456,6 +460,8 @@ describe('Maka Pi TUI transcript', () => {
lastReason: null,
achievedAt: null,
pausedAt: null,
armedAt: null,
boundTurnId: null,
status: 'active' as const,
},
};
Expand Down
Loading