From 87dc6ec47b8deb3d52f69b10257598af425155e5 Mon Sep 17 00:00:00 2001 From: Adam Eivy Date: Sat, 15 Aug 2026 04:16:43 +0000 Subject: [PATCH 1/2] refactor: split CoS task metadata.context into prompt vs note fields (#4153) Claude-Session: https://claude.ai/code/session_01KjWBcEeePHxa3AXeH7Xhqx --- .changelog/next/changed-issue-4153.md | 1 + client/src/components/cos/tabs/TaskItem.jsx | 47 +++- .../src/components/cos/tabs/TaskItem.test.jsx | 62 ++++++ .../migrations/270-cos-task-prompt-split.js | 205 ++++++++++++++++++ .../270-cos-task-prompt-split.test.js | 164 ++++++++++++++ server/lib/README.md | 1 + server/lib/cosTaskPrompt.js | 109 ++++++++++ server/lib/cosTaskPrompt.test.js | 103 +++++++++ server/lib/cosValidation.js | 5 + server/lib/index.js | 1 + server/lib/schemaVersions.js | 13 +- server/lib/taskParser.test.js | 15 ++ server/routes/cos.test.js | 16 +- server/routes/cosTaskRoutes.js | 11 +- server/services/agentManagement.js | 13 +- server/services/agentModelSelection.js | 5 +- server/services/agentPromptBuilder.js | 60 +++-- server/services/agentPromptBuilder.test.js | 73 ++++++- server/services/cos.test.js | 10 +- server/services/cosTaskGenerator.js | 18 +- server/services/cosTaskMerge.js | 6 +- server/services/cosTaskMerge.test.js | 21 ++ server/services/cosTaskStore.js | 28 ++- server/services/cosTaskStore.test.js | 68 ++++++ server/services/referenceRepos.js | 3 + server/services/repoIntake.js | 2 + server/services/sharing/cosTasksSync.test.js | 4 +- 27 files changed, 1001 insertions(+), 63 deletions(-) create mode 100644 .changelog/next/changed-issue-4153.md create mode 100644 scripts/migrations/270-cos-task-prompt-split.js create mode 100644 scripts/migrations/270-cos-task-prompt-split.test.js create mode 100644 server/lib/cosTaskPrompt.js create mode 100644 server/lib/cosTaskPrompt.test.js diff --git a/.changelog/next/changed-issue-4153.md b/.changelog/next/changed-issue-4153.md new file mode 100644 index 0000000000..94affae05d --- /dev/null +++ b/.changelog/next/changed-issue-4153.md @@ -0,0 +1 @@ +- CoS tasks: split the overloaded `metadata.context` into `metadata.prompt` (the full agent-facing payload) and `metadata.context` (a one-line human note), with a reader fallback for pre-split tasks and a migration for existing queues (#4153) diff --git a/client/src/components/cos/tabs/TaskItem.jsx b/client/src/components/cos/tabs/TaskItem.jsx index d97ef9ff7e..d82bcf262f 100644 --- a/client/src/components/cos/tabs/TaskItem.jsx +++ b/client/src/components/cos/tabs/TaskItem.jsx @@ -72,8 +72,16 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio setEditingInternal(val); onEditingChange?.(val); }, [onEditingChange]); + // A task written since the #4153 split keeps its full agent-facing payload in + // `metadata.prompt` and only a short human note in `metadata.context`. Legacy + // tasks (and peers still on the old code) have no `prompt` at all — their + // payload is still in `context` — so the Prompt field is offered ONLY when the + // task actually carries one, and is omitted from the PATCH otherwise rather + // than writing an empty `prompt` key onto every task the user edits. + const hasPromptField = typeof task.metadata?.prompt === 'string'; const [editData, setEditData] = useState({ description: task.description, + prompt: task.metadata?.prompt || '', context: task.metadata?.context || '', model: task.metadata?.model || '', provider: task.metadata?.provider || '' @@ -172,7 +180,9 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio }; const handleSave = async () => { - const result = await api.updateCosTask(task.id, { ...editData, type: taskSource }, { silent: true }).catch(err => { + const { prompt, ...rest } = editData; + const payload = hasPromptField ? { ...rest, prompt, type: taskSource } : { ...rest, type: taskSource }; + const result = await api.updateCosTask(task.id, payload, { silent: true }).catch(err => { toast.error(err.message); return null; }); @@ -187,6 +197,7 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio // changed something, so an unmodified Cancel still discards with no friction. const hasUnsavedEdits = editData.description !== task.description || + (hasPromptField && editData.prompt !== (task.metadata?.prompt || '')) || editData.context !== (task.metadata?.context || '') || editData.model !== (task.metadata?.model || '') || editData.provider !== (task.metadata?.provider || ''); @@ -207,6 +218,7 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio confirmDiscard(() => { setEditData({ description: task.description, + prompt: task.metadata?.prompt || '', context: task.metadata?.context || '', model: task.metadata?.model || '', provider: task.metadata?.provider || '' @@ -339,13 +351,24 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio onChange={e => setEditData(d => ({ ...d, description: e.target.value }))} className="w-full px-2 py-1 bg-port-bg border border-port-border rounded text-white text-sm" /> - {/* A textarea, not an input: for orchestrator tasks the context - holds the task's entire multi-line prompt, which is unreadable - and unnavigable in a single-line field. Bounded rows + its own + {/* A textarea, not an input: for orchestrator tasks this holds the + task's entire multi-line prompt, which is unreadable and + unnavigable in a single-line field. Bounded rows + its own scroll so editing a long prompt doesn't stretch the card. */} + {hasPromptField && ( +