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
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export interface PromptEditorProps extends PromptEditorKeyPolicy {
* Renders the editor as a non-editable display surface: the textarea becomes
* `readOnly` (so the chip overlay still paints `@`-mention / `/`-skill chips
* and the text stays selectable/copyable) and the caret-anchored resource and
* skill menus are not mounted. Use for read-only records — e.g. a finished
* scheduled task — where the prompt should render with chips but not be edited.
* skill menus are not mounted. Use for records where the prompt should render
* with chips but not be edited.
*/
readOnly?: boolean
/**
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/api/contracts/mothership-chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ export const mothershipExecuteBodySchema = z.object({
fileAttachments: z.array(mothershipExecuteFileAttachmentSchema).optional(),
/**
* `@`-mentioned resources / `/`-invoked skills to resolve into the agent run,
* mirroring the interactive chat path. Used by scheduled tasks, whose
* captured contexts must reach the run without a live client.
* mirroring the interactive chat path. Headless executions use this to pass
* captured contexts into the run without a live client.
*/
contexts: z.array(scheduleContextSchema).optional(),
mcpTools: z.array(mothershipExecuteMcpToolSchema).optional(),
Expand Down
40 changes: 0 additions & 40 deletions apps/sim/lib/copilot/chat/workspace-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,26 +166,6 @@ describe('buildWorkspaceMd - determinism (prompt-cache stability)', () => {
{ id: 'sk-2', name: 'Writer', description: 'writes' },
{ id: 'sk-1', name: 'Editor', description: 'edits' },
],
jobs: [
{
id: 'j-2',
title: 'Nightly',
prompt: 'run nightly',
cronExpression: '0 0 * * *',
status: 'active',
lifecycle: 'persistent',
sourceTaskName: null,
},
{
id: 'j-1',
title: 'Hourly',
prompt: 'run hourly',
cronExpression: '0 * * * *',
status: 'active',
lifecycle: 'persistent',
sourceTaskName: null,
},
],
})
)
const b = buildWorkspaceMd(
Expand Down Expand Up @@ -223,26 +203,6 @@ describe('buildWorkspaceMd - determinism (prompt-cache stability)', () => {
{ id: 'sk-1', name: 'Editor', description: 'edits' },
{ id: 'sk-2', name: 'Writer', description: 'writes' },
],
jobs: [
{
id: 'j-1',
title: 'Hourly',
prompt: 'run hourly',
cronExpression: '0 * * * *',
status: 'active',
lifecycle: 'persistent',
sourceTaskName: null,
},
{
id: 'j-2',
title: 'Nightly',
prompt: 'run nightly',
cronExpression: '0 0 * * *',
status: 'active',
lifecycle: 'persistent',
sourceTaskName: null,
},
],
})
)
expect(a).toBe(b)
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/lib/copilot/request/tools/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ export const TOOL_AWAITING_APPROVAL_STATUS = MothershipStreamV1ToolStatus.awaiti
/**
* Whether this call must be held for an explicit user decision.
*
* Headless and non-interactive runs (scheduled tasks, one-shot execute) are
* never gated: nobody is there to answer, and blocking them would hang the run
* until the orchestration timeout.
* Headless one-shot executions are never gated: nobody is there to answer, and
* blocking them would hang the run until the orchestration timeout.
*/
export function toolCallNeedsApproval(
toolName: string,
Expand Down
4 changes: 1 addition & 3 deletions apps/sim/lib/copilot/tools/handlers/function-execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,7 @@ function unmountableNamespaceReason(filePath: string): string | null {
if (path.startsWith('tables/')) {
return 'tables are not mounted as files. Pass the table in inputs.tables instead and it is mounted as CSV.'
}
const namespace = /^(workflows|knowledgebases|components|environment|agent|jobs|tasks)\//.exec(
path
)?.[1]
const namespace = /^(workflows|knowledgebases|components|environment|agent)\//.exec(path)?.[1]
if (namespace) {
return `${namespace}/ paths are VFS metadata views, not stored file bytes, so the sandbox cannot mount them. This path is correct — read or grep it and inline the values you need in code.`
}
Expand Down
1 change: 0 additions & 1 deletion apps/sim/lib/copilot/tools/tool-display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ describe('getToolDisplayTitle natural-language coverage', () => {
'Managing table',
'Preparing file',
'Processing media',
'Scheduled task action',
'Skill action',
])
const unresolvedVariants: string[] = []
Expand Down
54 changes: 0 additions & 54 deletions apps/sim/lib/copilot/vfs/serializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1115,57 +1115,3 @@ export function serializeTriggerOverview(
lines.push('')
return lines.join('\n')
}

export function serializeTaskSession(task: {
id: string
title: string
messageCount: number
createdAt: Date
updatedAt: Date
}): string {
return [
`# ${task.title}`,
'',
`- **Chat ID:** ${task.id}`,
`- **Created:** ${task.createdAt.toISOString()}`,
`- **Updated:** ${task.updatedAt.toISOString()}`,
`- **Messages:** ${task.messageCount}`,
'',
].join('\n')
}

export function serializeTaskChat(rawMessages: unknown[]): string {
const filtered: { role: string; content: string }[] = []

for (const msg of rawMessages) {
if (!msg || typeof msg !== 'object') continue
const m = msg as Record<string, unknown>
const role = m.role as string | undefined
if (role !== 'user' && role !== 'assistant') continue

let content = ''
if (role === 'assistant' && Array.isArray(m.contentBlocks)) {
const textParts: string[] = []
for (const block of m.contentBlocks) {
if (
block &&
typeof block === 'object' &&
(block as any).type === 'text' &&
(block as any).content
) {
textParts.push((block as any).content)
}
}
content = textParts.join('')
}

if (!content && typeof m.content === 'string') {
content = m.content
}

if (!content) continue
filtered.push({ role, content })
}

return JSON.stringify(filtered, null, 2)
}
94 changes: 1 addition & 93 deletions apps/sim/lib/copilot/vfs/workspace-vfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { trace } from '@opentelemetry/api'
import { db } from '@sim/db'
import {
chat as chatTable,
copilotChats,
customTools as customToolsTable,
document,
folder as folderTable,
Expand All @@ -17,7 +16,7 @@ import {
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { and, desc, eq, inArray, isNotNull, isNull, or, sql } from 'drizzle-orm'
import { and, desc, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm'
import { listApiKeys } from '@/lib/api-key/service'
import {
buildWorkspaceContextMd,
Expand Down Expand Up @@ -79,8 +78,6 @@ import {
serializeRecentExecutions,
serializeSkill,
serializeTableMeta,
serializeTaskChat,
serializeTaskSession,
serializeTriggerOverview,
serializeTriggerSchema,
serializeVersions,
Expand Down Expand Up @@ -521,8 +518,6 @@ function getStaticComponentFiles(): Map<string, string> {
* files/{name} (workspace file leaf; dynamic content on read)
* files/{path}/{name}/style (dynamic — style extraction for .docx/.pptx/.pdf)
* files/{path}/{name}/compiled-check (dynamic — compile generated source / validate diagrams, returns {ok,error?})
* tasks/{title}/session.md
* tasks/{title}/chat.json
* custom-tools/{name}.json
* environment/credentials.json
* environment/api-keys.json
Expand Down Expand Up @@ -777,10 +772,6 @@ export class WorkspaceVFS {
timed('workspace_row', getWorkspaceWithOwner(workspaceId)),
timed('members', getUsersWithPermissions(workspaceId)),
permissionConfigPromise,
// Writes tasks/ files only — WORKSPACE.md has no Tasks section
// (recent chats reorder every turn and would bust the cached
// prompt prefix), so nothing is destructured from this one.
timed('tasks', this.materializeTasks(workspaceId, userId)),
])

const workspaceMdData = {
Expand Down Expand Up @@ -2116,89 +2107,6 @@ export class WorkspaceVFS {
}
}

/**
* Materialize mothership task chats as browsable conversation files under
* `tasks/{title}/`. Nothing is returned: the inventory deliberately has no
* Tasks section, so these files are reached through glob/read only.
*/
private async materializeTasks(workspaceId: string, userId: string): Promise<void> {
try {
const taskRows = await db
.select({
id: copilotChats.id,
title: copilotChats.title,
messageCount: sql<number>`COALESCE((
SELECT COUNT(*) FROM copilot_messages cm
WHERE cm.chat_id = ${copilotChats.id} AND cm.deleted_at IS NULL
), 0)`,
messages: sql<unknown[]>`COALESCE((
SELECT jsonb_agg(
jsonb_build_object(
'role', cm.content->>'role',
'content', cm.content->'content',
'contentBlocks', COALESCE((
SELECT jsonb_agg(jsonb_build_object('type', 'text', 'content', b.value->'content') ORDER BY b.ord)
FROM jsonb_array_elements(
CASE WHEN jsonb_typeof(cm.content->'contentBlocks') = 'array'
THEN cm.content->'contentBlocks'
ELSE '[]'::jsonb
END
) WITH ORDINALITY AS b(value, ord)
WHERE b.value->>'type' = 'text'
), '[]'::jsonb)
)
ORDER BY cm.seq ASC NULLS LAST, cm.created_at ASC, cm.id ASC
)
FROM copilot_messages cm
WHERE cm.chat_id = ${copilotChats.id}
AND cm.deleted_at IS NULL
AND cm.content->>'role' IN ('user', 'assistant')
), '[]'::jsonb)`,
createdAt: copilotChats.createdAt,
updatedAt: copilotChats.updatedAt,
})
.from(copilotChats)
.where(
and(
eq(copilotChats.workspaceId, workspaceId),
eq(copilotChats.userId, userId),
eq(copilotChats.type, 'mothership'),
isNull(copilotChats.deletedAt)
)
)
.orderBy(desc(copilotChats.updatedAt))
.limit(5)

for (const task of taskRows) {
const title = task.title || 'Untitled task'
const safeName = sanitizeName(title)
const prefix = `tasks/${safeName}/`
const messages = Array.isArray(task.messages) ? task.messages : []
const messageCount = Number(task.messageCount) || 0

this.files.set(
`${prefix}session.md`,
serializeTaskSession({
id: task.id,
title,
messageCount,
createdAt: task.createdAt,
updatedAt: task.updatedAt,
})
)

if (messages.length > 0) {
this.files.set(`${prefix}chat.json`, serializeTaskChat(messages))
}
}
} catch (err) {
logger.warn('Failed to materialize tasks', {
workspaceId,
error: toError(err).message,
})
}
}

private async materializeRecentlyDeleted(workspaceId: string, userId: string): Promise<void> {
try {
const [
Expand Down
Loading