Skip to content

Commit dfe879c

Browse files
committed
fix(mothership): streaming invariant projection enforcement
1 parent 9199758 commit dfe879c

69 files changed

Lines changed: 8680 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/components/icons.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,24 @@ export function WhatsAppIcon(props: SVGProps<SVGSVGElement>) {
19581958
)
19591959
}
19601960

1961+
export function SportmonksIcon(props: SVGProps<SVGSVGElement>) {
1962+
return (
1963+
<svg
1964+
{...props}
1965+
viewBox='0 0 25 24'
1966+
fill='none'
1967+
fillRule='evenodd'
1968+
xmlns='http://www.w3.org/2000/svg'
1969+
>
1970+
<path
1971+
d='M11.857 8.546c1.893 0 3.517.678 4.872 2.033 1.355 1.336 2.032 2.96 2.032 4.872 0 1.91-.677 3.535-2.032 4.871-1.355 1.355-2.979 2.032-4.872 2.032H1V17.093h10.857c.446 0 .825-.157 1.142-.473.334-.334.5-.724.5-1.17 0-.445-.166-.835-.5-1.17a1.558 1.558 0 00-1.142-.472H7.905c-1.912 0-3.537-.677-4.873-2.032C1.678 10.421 1 8.796 1 6.903 1 4.993 1.678 3.368 3.033 2.032 4.368.678 5.992 0 7.905 0h10.188V5.263H7.904a1.65 1.65 0 00-1.17.473 1.586 1.586 0 00-.472 1.169c0 .445.157.835.473 1.17.334.315.724.472 1.17.472h3.952z'
1972+
fill='currentColor'
1973+
/>
1974+
<circle cx='21.27' cy='20.123' r='2.732' fill='#FF0F50' />
1975+
</svg>
1976+
)
1977+
}
1978+
19611979
export function SquareIcon(props: SVGProps<SVGSVGElement>) {
19621980
return (
19631981
<svg {...props} viewBox='0 0 501.42 501.42' xmlns='http://www.w3.org/2000/svg'>

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ import {
190190
SixtyfourIcon,
191191
SlackIcon,
192192
SmtpIcon,
193+
SportmonksIcon,
193194
SQSIcon,
194195
SquareIcon,
195196
SshIcon,
@@ -439,6 +440,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
439440
sixtyfour: SixtyfourIcon,
440441
slack: SlackIcon,
441442
smtp: SmtpIcon,
443+
sportmonks: SportmonksIcon,
442444
sqs: SQSIcon,
443445
square: SquareIcon,
444446
ssh: SshIcon,

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
"sixtyfour",
192192
"slack",
193193
"smtp",
194+
"sportmonks",
194195
"sqs",
195196
"square",
196197
"ssh",

apps/docs/content/docs/en/integrations/sportmonks.mdx

Lines changed: 1517 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,24 @@ describe('reduceEvent — edit_content row merge', () => {
381381
expect(tool(m, 'wf-1').result?.success).toBe(true)
382382
expect(m.bufferedResults.has('ec-1')).toBe(false)
383383
})
384+
385+
it('finalizes a stale running section row when the next section opens', () => {
386+
const sub: Scope = { lane: 'subagent', spanId: 'S1' }
387+
const m = apply([
388+
spanStart(1, 'S1', 'file', 'tc-file'),
389+
// Section 1: the workspace_file row is reopened by its edit_content, but the
390+
// edit's closing result is reordered/dropped — wf-1 is left running.
391+
toolCall(2, 'wf-1', 'workspace_file', sub),
392+
toolResult(3, 'wf-1', true, undefined, sub),
393+
toolCall(4, 'ec-1', 'edit_content', sub),
394+
// Section 2 opens before section 1's edit result lands.
395+
toolCall(5, 'wf-2', 'workspace_file', sub),
396+
])
397+
// The previous section settles instead of spinning until the turn terminal...
398+
expect(tool(m, 'wf-1').status).toBe('success')
399+
// ...and the new section's row is the live write.
400+
expect(tool(m, 'wf-2').status).toBe('running')
401+
})
384402
})
385403

386404
describe('reduceEvent — error tag + compaction coverage', () => {

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ function findWorkspaceFileNodeInSpan(model: TurnModel, spanId: string): ToolNode
150150
return undefined
151151
}
152152

153+
/**
154+
* The file agent writes a file as strictly sequential `workspace_file` +
155+
* `edit_content` section pairs, waiting for each to finish before the next. So
156+
* when a new section's `workspace_file` opens, any earlier `workspace_file` row
157+
* still `running` in the same span is a completed section whose closing
158+
* `edit_content` result was reordered or dropped — finalize it as success so its
159+
* "writing" spinner resolves when the next section starts, instead of lingering
160+
* until the turn-terminal sweep. A no-op on the happy path (prior rows already
161+
* settled on their own result).
162+
*/
163+
function finalizeStaleWorkspaceFiles(model: TurnModel, spanId: string): void {
164+
for (const id of model.order) {
165+
const node = model.nodes.get(id)
166+
if (
167+
node?.kind === 'tool' &&
168+
node.spanId === spanId &&
169+
node.name === WORKSPACE_FILE_TOOL &&
170+
node.status === 'running'
171+
) {
172+
node.status = 'success'
173+
node.streamingArgs = undefined
174+
}
175+
}
176+
}
177+
153178
function isRecord(value: unknown): value is Record<string, unknown> {
154179
return typeof value === 'object' && value !== null && !Array.isArray(value)
155180
}
@@ -438,6 +463,11 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve
438463
break
439464
}
440465
}
466+
// A new file section opening settles any earlier still-running section row
467+
// in this span (the file agent writes sections sequentially).
468+
if (toolName === WORKSPACE_FILE_TOOL && !model.nodes.has(rawToolCallId)) {
469+
finalizeStaleWorkspaceFiles(model, spanId)
470+
}
441471
const node = upsertToolNode(
442472
model,
443473
resolveToolId(model, rawToolCallId),

0 commit comments

Comments
 (0)