Skip to content

Commit a041474

Browse files
committed
refactor: consolidate local isRecord guards onto shared isRecordLike
Nineteen files had re-declared a local `isRecord` guard rather than using the shared one from `@sim/utils/object`, drift that reappeared after #5061 first consolidated them. Two more imported the shared guard under an `isRecordLike as isRecord` alias. The copies were not interchangeable. Nine matched `isRecordLike` exactly. The rest omitted the array exclusion (`typeof x === 'object' && x !== null`, or `Boolean(x) && typeof x === 'object'`), so arrays passed the guard. Each of those call sites was reviewed individually: in every case the guard is followed by string/number field checks that an array fails anyway, so the outcome is unchanged. The one exception is `isOptionsTagData`, where `Object.values` on an array of option items really did make an array-form `<options>` tag render. It now accepts arrays explicitly rather than by accident. `executor/handlers/pi/search/extension-source.ts` keeps its own copy: it is source text written into an E2B/Daytona sandbox at runtime and cannot import.
1 parent 3848f97 commit a041474

36 files changed

Lines changed: 213 additions & 245 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
toast,
1515
} from '@sim/emcn'
1616
import { TerminalWindow } from '@sim/emcn/icons'
17+
import { isRecordLike } from '@sim/utils/object'
1718
import { useParams } from 'next/navigation'
1819
import { ThinkingLoader } from '@/components/ui'
1920
import { useSession } from '@/lib/auth/auth-client'
@@ -353,22 +354,23 @@ export const SPECIAL_TAG_NAMES = [
353354
'question',
354355
] as const
355356

356-
function isRecord(value: unknown): value is Record<string, unknown> {
357-
return typeof value === 'object' && value !== null
358-
}
359-
360357
function isOptionsItemData(value: unknown): value is OptionsItemData {
361-
if (!isRecord(value)) return false
358+
if (!isRecordLike(value)) return false
362359
return typeof value.title === 'string' && typeof value.description === 'string'
363360
}
364361

362+
/**
363+
* Arrays are accepted alongside keyed objects: an agent that emits
364+
* `<options>[{title,description},…]</options>` still renders, with the array
365+
* index standing in as the option key.
366+
*/
365367
function isOptionsTagData(value: unknown): value is OptionsTagData {
366-
if (!isRecord(value)) return false
368+
if (!isRecordLike(value) && !Array.isArray(value)) return false
367369
return Object.values(value).every(isOptionsItemData)
368370
}
369371

370372
function isUsageUpgradeTagData(value: unknown): value is UsageUpgradeTagData {
371-
if (!isRecord(value)) return false
373+
if (!isRecordLike(value)) return false
372374
return (
373375
typeof value.reason === 'string' &&
374376
typeof value.message === 'string' &&
@@ -378,7 +380,7 @@ function isUsageUpgradeTagData(value: unknown): value is UsageUpgradeTagData {
378380
}
379381

380382
function isCredentialItemData(value: unknown): value is CredentialItemData {
381-
if (!isRecord(value)) return false
383+
if (!isRecordLike(value)) return false
382384
if (
383385
typeof value.type !== 'string' ||
384386
!(CREDENTIAL_TAG_TYPES as readonly string[]).includes(value.type)
@@ -452,7 +454,7 @@ export function parseLastCredentialTag(content: string): CredentialTagData | nul
452454
}
453455

454456
function isMothershipErrorTagData(value: unknown): value is MothershipErrorTagData {
455-
if (!isRecord(value)) return false
457+
if (!isRecordLike(value)) return false
456458
return (
457459
typeof value.message === 'string' &&
458460
(value.code === undefined || typeof value.code === 'string') &&
@@ -461,7 +463,7 @@ function isMothershipErrorTagData(value: unknown): value is MothershipErrorTagDa
461463
}
462464

463465
function isWorkspaceResourceTagData(value: unknown): value is WorkspaceResourceTagData {
464-
if (!isRecord(value)) return false
466+
if (!isRecordLike(value)) return false
465467
if (
466468
typeof value.type !== 'string' ||
467469
!(WORKSPACE_RESOURCE_TAG_TYPES as readonly string[]).includes(value.type)
@@ -479,7 +481,7 @@ function isWorkspaceResourceTagData(value: unknown): value is WorkspaceResourceT
479481
}
480482

481483
function isQuestionOption(value: unknown): value is QuestionOption {
482-
if (!isRecord(value)) return false
484+
if (!isRecordLike(value)) return false
483485
return typeof value.id === 'string' && typeof value.label === 'string'
484486
}
485487

@@ -497,7 +499,7 @@ const SELF_PROVIDED_OPTION_LABELS = new Set([
497499
])
498500

499501
function isQuestionItem(value: unknown): value is QuestionItem {
500-
if (!isRecord(value)) return false
502+
if (!isRecordLike(value)) return false
501503
if (
502504
typeof value.type !== 'string' ||
503505
!(QUESTION_TYPES as readonly string[]).includes(value.type)
@@ -551,7 +553,7 @@ function recoverQuestionPrompts(body: string): string | null {
551553
const parsed = JSON.parse(body) as unknown
552554
const items = Array.isArray(parsed) ? parsed : [parsed]
553555
const prompts = items
554-
.filter(isRecord)
556+
.filter(isRecordLike)
555557
.map((item) => (typeof item.prompt === 'string' ? item.prompt.trim() : ''))
556558
.filter((prompt) => prompt.length > 0)
557559
return prompts.length > 0 ? prompts.join('\n\n') : null

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isRecordLike as isRecord } from '@sim/utils/object'
1+
import { isRecordLike } from '@sim/utils/object'
22
import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome'
33
import {
44
MothershipStreamV1CompletionStatus,
@@ -220,10 +220,10 @@ function rebindResolvedIntegrationCall(node: ToolNode, toolName: string): void {
220220
/**
221221
* Reads a wire event payload as a generic record. The payload is a wide
222222
* discriminated union; the reducer accesses fields uniformly, so this narrows
223-
* through the `unknown`-typed {@link isRecord} guard rather than a double cast.
223+
* through the `unknown`-typed {@link isRecordLike} guard rather than a double cast.
224224
*/
225225
function payloadRecord(payload: unknown): Record<string, unknown> {
226-
return isRecord(payload) ? payload : {}
226+
return isRecordLike(payload) ? payload : {}
227227
}
228228

229229
/** Parses a wire `ts` to epoch ms, or undefined when absent/unparseable. */
@@ -523,15 +523,15 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
523523
// back into an ordinary running row without waiting for the result.
524524
node.status = 'running'
525525
}
526-
if (isRecord(payload.arguments)) node.args = payload.arguments
526+
if (isRecordLike(payload.arguments)) node.args = payload.arguments
527527
// Only the snapshot-replay path (contentBlocksToModel) carries this
528528
// field — the live wire never does; it restores the rebound gateway
529529
// description across a preserve-state rebuild.
530530
const restoredDescription = asString(payload.integrationDescription)
531531
if (restoredDescription) node.integrationDescription = restoredDescription
532532
// Tool-call titles are derived from the tool name (+args) at serialize
533533
// time; the stream only carries behavioral flags now.
534-
const ui = isRecord(payload.ui) ? payload.ui : undefined
534+
const ui = isRecordLike(payload.ui) ? payload.ui : undefined
535535
if (ui?.hidden === true) node.hidden = true
536536
} else if (phase === MothershipStreamV1ToolPhase.args_delta) {
537537
const node = upsertToolNode(
@@ -559,7 +559,7 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
559559
case MothershipStreamV1EventType.span: {
560560
const payload = envelope.payload
561561
if (payload.kind !== MothershipStreamV1SpanPayloadKind.subagent) break
562-
const data = isRecord(payload.data) ? payload.data : undefined
562+
const data = isRecordLike(payload.data) ? payload.data : undefined
563563
const triggerToolCallId =
564564
scope?.parentToolCallId ?? asString(data?.tool_call_id) ?? asString(data?.toolCallId)
565565
const agentId = asString(payload.agent) ?? scope?.agentId ?? ''
@@ -686,7 +686,7 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
686686
const payload = payloadRecord(envelope.payload)
687687
// An async pause is not a turn terminal — the paused tools/subagents
688688
// legitimately stay open until a later resume leg completes them.
689-
const response = isRecord(payload.response) ? payload.response : undefined
689+
const response = isRecordLike(payload.response) ? payload.response : undefined
690690
if (response && 'async_pause' in response) break
691691
const status = payload.status
692692
if (status === MothershipStreamV1CompletionStatus.cancelled) {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { getErrorMessage, toError } from '@sim/utils/errors'
55
import { sleep } from '@sim/utils/helpers'
66
import { generateId } from '@sim/utils/id'
7+
import { isRecordLike } from '@sim/utils/object'
78
import { useQueryClient } from '@tanstack/react-query'
89
import { useParams } from 'next/navigation'
910
import { useShallow } from 'zustand/react/shallow'
@@ -132,10 +133,6 @@ async function persistExecutionPointerProgress(
132133
await saveExecutionPointer({ workflowId, executionId, lastEventId })
133134
}
134135

135-
function isRecord(value: unknown): value is Record<string, unknown> {
136-
return typeof value === 'object' && value !== null
137-
}
138-
139136
function isRecoverableStreamRecoveryError(
140137
error: unknown
141138
): error is SSEEventHandlerError | SSEStreamInterruptedError {
@@ -158,12 +155,12 @@ function normalizeErrorMessage(error: unknown): string {
158155
if (message) return message
159156
}
160157

161-
if (isRecord(error)) {
158+
if (isRecordLike(error)) {
162159
const directMessage = sanitizeMessage(error.message)
163160
if (directMessage) return directMessage
164161

165162
const nestedError = error.error
166-
if (isRecord(nestedError)) {
163+
if (isRecordLike(nestedError)) {
167164
const nestedMessage = sanitizeMessage(nestedError.message)
168165
if (nestedMessage) return nestedMessage
169166
} else {
@@ -181,7 +178,7 @@ interface ChatWorkflowInput {
181178
}
182179

183180
function isChatWorkflowInput(value: unknown): value is ChatWorkflowInput {
184-
return isRecord(value) && 'input' in value
181+
return isRecordLike(value) && 'input' in value
185182
}
186183

187184
export interface ChatWorkflowRunResult {
@@ -199,7 +196,7 @@ export class WorkflowAttachmentUploadError extends Error {
199196

200197
export function isChatWorkflowRunResult(value: unknown): value is ChatWorkflowRunResult {
201198
return (
202-
isRecord(value) &&
199+
isRecordLike(value) &&
203200
value.success === true &&
204201
value.stream instanceof ReadableStream &&
205202
Array.isArray(value.uploadedAttachments)
@@ -1688,10 +1685,11 @@ export function useWorkflowExecution() {
16881685
}
16891686

16901687
let notificationMessage = WORKFLOW_EXECUTION_FAILURE_MESSAGE
1691-
const requestError = isRecord(error) && isRecord(error.request) ? error.request : undefined
1688+
const requestError =
1689+
isRecordLike(error) && isRecordLike(error.request) ? error.request : undefined
16921690
if (requestError && sanitizeMessage(requestError.url)) {
16931691
notificationMessage += `: Request to ${(requestError.url as string).trim()} failed`
1694-
if (isRecord(error) && typeof error.status === 'number') {
1692+
if (isRecordLike(error) && typeof error.status === 'number') {
16951693
notificationMessage += ` (Status: ${error.status})`
16961694
}
16971695
} else if (sanitizeMessage(errorResult.error)) {

apps/sim/ee/workspace-forking/lib/copy/cleanup-failed.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {
99
} from '@sim/db/schema'
1010
import { createLogger } from '@sim/logger'
1111
import { getErrorMessage } from '@sim/utils/errors'
12+
import { isRecordLike } from '@sim/utils/object'
1213
import { and, asc, eq, exists, gt, inArray, isNull, notExists, sql } from 'drizzle-orm'
13-
import { isRecord, type SubBlockRecord } from '@/lib/workflows/persistence/remap-internal-ids'
14+
import type { SubBlockRecord } from '@/lib/workflows/persistence/remap-internal-ids'
1415
import { invalidateDeployedStateCache } from '@/lib/workflows/persistence/utils'
1516
import {
1617
FORK_DOCUMENT_ID_PATTERN,
@@ -325,13 +326,13 @@ export function rewriteDeploymentVersionState(
325326
state: unknown,
326327
resolve: ForkCopyResolver
327328
): { state: unknown; changed: boolean } {
328-
if (!isRecord(state) || !isRecord(state.blocks)) return { state, changed: false }
329+
if (!isRecordLike(state) || !isRecordLike(state.blocks)) return { state, changed: false }
329330

330331
let nextBlocks: Record<string, unknown> | null = null
331332
for (const [blockId, block] of Object.entries(state.blocks)) {
332-
if (!isRecord(block)) continue
333+
if (!isRecordLike(block)) continue
333334
const blockType = typeof block.type === 'string' ? block.type : undefined
334-
if (!blockType || !isRecord(block.subBlocks)) continue
335+
if (!blockType || !isRecordLike(block.subBlocks)) continue
335336
const { subBlocks: cleared, changed } = clearFailedSubBlockReferences(
336337
block.subBlocks as SubBlockRecord,
337338
blockType,

apps/sim/ee/workspace-forking/lib/copy/copy-chats.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { chat } from '@sim/db/schema'
22
import { createLogger } from '@sim/logger'
33
import { generateId, generateShortId } from '@sim/utils/id'
4+
import { isRecordLike } from '@sim/utils/object'
45
import { randomInt } from '@sim/utils/random'
56
import { and, inArray, isNull } from 'drizzle-orm'
67
import type { DbOrTx } from '@/lib/db/types'
7-
import { isRecord } from '@/lib/workflows/persistence/remap-internal-ids'
88

99
const logger = createLogger('WorkspaceForkCopyChats')
1010

@@ -47,7 +47,7 @@ function remapChatOutputConfigs(
4747
): unknown {
4848
if (!Array.isArray(value)) return value
4949
return value.map((entry) => {
50-
if (!isRecord(entry) || typeof entry.blockId !== 'string') return entry
50+
if (!isRecordLike(entry) || typeof entry.blockId !== 'string') return entry
5151
return { ...entry, blockId: resolveBlockId(targetWorkflowId, entry.blockId) }
5252
})
5353
}

apps/sim/ee/workspace-forking/lib/copy/copy-resources.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { createLogger } from '@sim/logger'
2020
import { sha256Hex } from '@sim/security/hash'
2121
import { getErrorMessage } from '@sim/utils/errors'
2222
import { generateId } from '@sim/utils/id'
23-
import { omit } from '@sim/utils/object'
23+
import { isRecordLike, omit } from '@sim/utils/object'
2424
import {
2525
and,
2626
asc,
@@ -71,7 +71,6 @@ import {
7171
recordKnowledgeBaseFileOwnership,
7272
} from '@/lib/uploads/server/metadata'
7373
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
74-
import { isRecord } from '@/lib/workflows/persistence/remap-internal-ids'
7574
import {
7675
deleteCopiedResourceMappingsByTargets,
7776
type ForkMappingUpsert,
@@ -538,7 +537,7 @@ export async function copyForkResourceContainers(
538537
const inserts: (typeof mcpServers.$inferInsert)[] = []
539538
for (const row of rows) {
540539
const childId = generateId()
541-
const headers = isRecord(row.headers)
540+
const headers = isRecordLike(row.headers)
542541
? Object.fromEntries(
543542
Object.entries(row.headers).map(([key, value]) => [
544543
key,
@@ -942,7 +941,7 @@ function remapTableRowResourceUrls(value: unknown, maps: ForkContentRefMaps): un
942941
})
943942
return changed ? next : value
944943
}
945-
if (isRecord(value)) {
944+
if (isRecordLike(value)) {
946945
let changed = false
947946
const next: Record<string, unknown> = {}
948947
for (const [key, item] of Object.entries(value)) {

apps/sim/ee/workspace-forking/lib/mapping/dependent-reconfigs.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { isRecordLike } from '@sim/utils/object'
12
import type { ForkDependentReconfig, ForkResourceUsage } from '@/lib/api/contracts/workspace-fork'
2-
import { coerceObjectArray, isRecord } from '@/lib/workflows/persistence/remap-internal-ids'
3+
import { coerceObjectArray } from '@/lib/workflows/persistence/remap-internal-ids'
34
import { getWorkflowSearchDependentClears } from '@/lib/workflows/search-replace/dependencies'
45
import {
56
buildSelectorContextFromBlock,
@@ -293,10 +294,10 @@ export function collectForkDependentReconfigs(
293294
if (!tools) continue
294295
for (let index = 0; index < tools.length; index++) {
295296
const tool = tools[index]
296-
if (!isRecord(tool) || typeof tool.type !== 'string') continue
297+
if (!isRecordLike(tool) || typeof tool.type !== 'string') continue
297298
const toolConfig = getBlock(tool.type)
298299
if (!toolConfig) continue
299-
const toolParams = isRecord(tool.params) ? tool.params : {}
300+
const toolParams = isRecordLike(tool.params) ? tool.params : {}
300301
// A tool's `operation` is stored at the tool level, not in params, but subblock
301302
// conditions reference it (e.g. a Gmail label only under `read_gmail`). Merge it
302303
// in so condition-gating matches the editor's `{ operation, ...params }`.

apps/sim/ee/workspace-forking/lib/promote/cleared-refs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { mcpServers, workflow } from '@sim/db/schema'
2+
import { isRecordLike } from '@sim/utils/object'
23
import { and, eq, inArray } from 'drizzle-orm'
34
import type {
45
ForkClearedRef,
@@ -8,7 +9,6 @@ import type {
89
import type { DbOrTx } from '@/lib/db/types'
910
import {
1011
coerceObjectArray,
11-
isRecord,
1212
type SubBlockRecord,
1313
} from '@/lib/workflows/persistence/remap-internal-ids'
1414
import {
@@ -136,9 +136,9 @@ function collectForkWorkflowReferences(
136136
if (!array) continue
137137
for (const tool of array) {
138138
if (
139-
isRecord(tool) &&
139+
isRecordLike(tool) &&
140140
tool.type === 'workflow_input' &&
141-
isRecord(tool.params) &&
141+
isRecordLike(tool.params) &&
142142
typeof tool.params.workflowId === 'string' &&
143143
tool.params.workflowId
144144
) {

0 commit comments

Comments
 (0)