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
1 change: 0 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ Internal backlog for Orchid. User-facing summary lives in the [README known limi
- Interrupted subagents are being marked as complete - possibly after starting a new chain it is not preserved? - but only on some places (subagent view is correct, main agent context and main chat/session UI appears to not be)
- replace_symbol can left trailing remnants
- crashed / closed app can make the agent lose context (Subagents - on the interface they still appear, for the main againt they do not - IDs not found)
- Remove todo status change restictions - only confuses the agent
- Are read results or other tools content being scaped, with the possibility of confusing the agent?

## Agent quality
Expand Down
30 changes: 3 additions & 27 deletions electron/src/main/tools/todo/store.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/**
* TodoStore — in-memory store for todo tasks with state machine validation.
* TodoStore — in-memory store for todo tasks.
*
* Ported from Python `src/orchid/domain/todo.py` (TodoStore class).
*
* Key behaviors (matching Python):
* Key behaviors:
* - Session-scoped in-memory store
* - 8-hex UUID generation with collision retry
* - State machine validation via VALID_TRANSITIONS
* - create(), get(), list(), update(), delete()
* - toData() for serialization (TodoStoreData)
*
Expand All @@ -16,7 +15,6 @@
import { randomUUID } from 'node:crypto';
import {
TodoStatus,
VALID_TRANSITIONS,
type Todo,
type TodoStoreData,
} from '../../../shared/types/todo';
Expand Down Expand Up @@ -108,8 +106,7 @@ export class TodoStore {
/**
* Update a task. Returns [task, error]. On success, error is null.
*
* Validates status transitions against VALID_TRANSITIONS.
* Terminal status tasks (DONE) cannot be updated.
* No status-transition restrictions — any status can go to any status.
*
* @param id - Task ID to update
* @param updates - Fields to update (title, status, subagent_id)
Expand All @@ -124,27 +121,6 @@ export class TodoStore {
return [null, `No task found with ID '${id}'.`];
}

// DONE is terminal — no transitions allowed (matches Python TERMINAL_STATUSES)
if (task.status === TodoStatus.DONE) {
return [
null,
`Task '${id}' is in terminal status '${task.status}' and cannot be updated.`,
];
}

// Validate status transition
if (updates.status !== undefined) {
const allowed = VALID_TRANSITIONS[task.status];
if (!allowed.has(updates.status)) {
const targets =
[...allowed].sort().join(', ') || 'none';
return [
null,
`Cannot transition from '${task.status}' to '${updates.status}'. Allowed: ${targets}`,
];
}
}

// Apply updates
const now = new Date().toISOString();
const updated: Todo = {
Expand Down
5 changes: 1 addition & 4 deletions electron/src/main/tools/todo/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ export function buildUpdateTool(
name: 'todo_update',
description:
'Update an existing task owned by the current agent.\n\n' +
'Status transitions:\n' +
` OPEN → IN_PROGRESS\n` +
` IN_PROGRESS → DONE\n` +
` DONE → (terminal, no transitions)`,
'Status transitions are unrestricted: any status can be set to any status.',
inputSchema: z.object({
id: z.string().describe('The ID of the task to update.'),
title: z.string().optional().describe('New title (optional).'),
Expand Down
16 changes: 2 additions & 14 deletions electron/src/shared/types/todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@
*
* Ported from src/orchid/domain/todo.py.
*
* The TodoStore is session-scoped and tracks task state transitions
* via VALID_TRANSITIONS (matching Python's state machine).
*
* Python has 7 statuses; the TS port includes all of them for
* storage-compat. The task description's minimal subset (OPEN,
* IN_PROGRESS, DONE) is the most commonly used.
* The TodoStore is session-scoped. Status is free-form: any status
* can transition to any status (no state-machine restrictions).
*/

// ── Enums as const objects ──────────────────────────────────────────────────
Expand All @@ -21,14 +17,6 @@ export const TodoStatus = {

export type TodoStatus = (typeof TodoStatus)[keyof typeof TodoStatus];

// ── Valid transitions ───────────────────────────────────────────────────────

export const VALID_TRANSITIONS: Record<TodoStatus, ReadonlySet<TodoStatus>> = {
[TodoStatus.OPEN]: new Set<TodoStatus>([TodoStatus.IN_PROGRESS]),
[TodoStatus.IN_PROGRESS]: new Set<TodoStatus>([TodoStatus.DONE]),
[TodoStatus.DONE]: new Set<TodoStatus>([]),
};

// ── Todo ────────────────────────────────────────────────────────────────────

export interface Todo {
Expand Down
25 changes: 12 additions & 13 deletions electron/tests/unit/todo-web-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* Tests for Todo & Web Tools (U15).
*
* Covers:
* - Todo: create → ID, OPEN status, OPEN → IN_PROGRESS → DONE (valid),
* DONE → IN_PROGRESS (invalid), list, delete
* - Todo: create → ID, OPEN status, free status transitions (any → any),
* list, delete
* - Web fetch: URL validation (scheme/empty only), summarize mode,
* raw mode, large content caching
*/
Expand Down Expand Up @@ -203,7 +203,7 @@ describe('Todo Tools', () => {
expect(store.get(id)!.status).toBe(TodoStatus.DONE);
});

it('should reject DONE → IN_PROGRESS transition', async () => {
it('should allow DONE → IN_PROGRESS transition (no restrictions)', async () => {
const createHandler = buildCreateTool(store).handler;
const createResult = (await callTool(createHandler, {
title: 'Test',
Expand All @@ -216,18 +216,18 @@ describe('Todo Tools', () => {
await callTool(updateHandler, { id, status: TodoStatus.IN_PROGRESS });
await callTool(updateHandler, { id, status: TodoStatus.DONE });

// Try to go back to IN_PROGRESS
// Go back to IN_PROGRESS — allowed
const result = (await callTool(updateHandler, {
id,
status: TodoStatus.IN_PROGRESS,
})) as ToolExecutionResult;

expect(result.canonical.status).toBe('error');
expect(result.agentProjection.content).toContain('terminal status');
expect(store.get(id)!.status).toBe(TodoStatus.DONE);
expect(result.canonical.status).toBe('complete');
expect(result.agentProjection.content).toContain('<status>IN_PROGRESS</status>');
expect(store.get(id)!.status).toBe(TodoStatus.IN_PROGRESS);
});

it('should reject OPEN → DONE transition (must go through IN_PROGRESS)', async () => {
it('should allow OPEN → DONE transition directly', async () => {
const createHandler = buildCreateTool(store).handler;
const createResult = (await callTool(createHandler, {
title: 'Test',
Expand All @@ -240,8 +240,9 @@ describe('Todo Tools', () => {
status: TodoStatus.DONE,
})) as ToolExecutionResult;

expect(result.canonical.status).toBe('error');
expect(result.agentProjection.content).toContain('Cannot transition');
expect(result.canonical.status).toBe('complete');
expect(result.agentProjection.content).toContain('<status>DONE</status>');
expect(store.get(id)!.status).toBe(TodoStatus.DONE);
});

it('should reject invalid status values at schema boundary', async () => {
Expand Down Expand Up @@ -288,9 +289,7 @@ describe('Todo Tools', () => {

it('should filter by status', async () => {
const createHandler = buildCreateTool(store).handler;
const r1 = (await callTool(createHandler, { title: 'Open task' })) as {
content: string;
};
await callTool(createHandler, { title: 'Open task' });
const r2 = (await callTool(createHandler, { title: 'Progress task' })) as {
content: string;
};
Expand Down
Loading