Let a node's failure outlive the run that followed it
A node's error cleared the moment it ran again, so a failure that genuinely fired an alert could leave no trace on the canvas by the time anyone looked. The engine records it now — on the node's status, so it survives a reload and every client agrees — and reading the traceback is what clears it. The seam is the event bus, which is where every failing path already meets: a queued live run, an explicit run, a preview, and a single triggered node all publish `node_error`, while the controller's own observer would have seen only one of them. That was half the confusion. The other half: clicking a failed neuron on Home often landed on a flow where everything looked fine. Nodes merge into one neuron by instance key — every InfluxDB node pointing at the same bucket is one neuron — and the click went to whichever flow contributed a member first, not the one that failed. It now goes to the failing member and selects it, and the canvas marks a failing node rather than leaving it to the dot alone. The inject node emitted one payload to every port it declared, whatever their types, so an inject on a bool port carrying the text "true" raised at publish time. Each port gets its own field now, typed and parsed by that port's dtype, and remembers what it last sent. A port that is renamed carries its value with it; one that is removed takes its value with it. An inject written before this keeps emitting exactly what it did. The derived-cron chip also appeared on the delay node, where `interval` is a rate limit and a schedule derived from it means nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { qualify } from "./deriveEdges"
|
||||
import { useLiveValue } from "./liveStore"
|
||||
import { asText, DTYPES } from "./NodePanel"
|
||||
@@ -46,6 +47,66 @@ export function parseByDtype(dtype: DType | undefined, raw: string): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One literal, entered the way its type is entered.
|
||||
*
|
||||
* A flag has two values and gets a choice of them; everything else is typed
|
||||
* and read back with {@link parseByDtype}. Deliberately not `type="number"`
|
||||
* for the numbers — see that function on why half-typed input must survive.
|
||||
*
|
||||
* `className` carries no height: the two controls need different ones.
|
||||
*/
|
||||
export function DtypeValue({
|
||||
dtype,
|
||||
value,
|
||||
label,
|
||||
id,
|
||||
placeholder,
|
||||
className,
|
||||
onChange,
|
||||
}: {
|
||||
dtype: DType | undefined
|
||||
value: unknown
|
||||
/** What the field is called, for anyone not looking at it. */
|
||||
label: string
|
||||
id?: string
|
||||
placeholder?: string
|
||||
className?: string
|
||||
onChange: (next: unknown) => void
|
||||
}) {
|
||||
if (dtype === "bool") {
|
||||
return (
|
||||
<Select
|
||||
value={value === true ? "true" : "false"}
|
||||
onValueChange={(next) => onChange(next === "true")}
|
||||
>
|
||||
<SelectTrigger
|
||||
id={id}
|
||||
className={cn("!h-8", className)}
|
||||
aria-label={label}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">true</SelectItem>
|
||||
<SelectItem value="false">false</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
value={asText(value)}
|
||||
placeholder={placeholder}
|
||||
aria-label={label}
|
||||
className={cn("h-8", className)}
|
||||
onChange={(event) => onChange(parseByDtype(dtype, event.target.value))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Putting a declared value into the running graph, credited to the input. */
|
||||
function usePublishInput() {
|
||||
return useMutation({
|
||||
@@ -116,38 +177,14 @@ function InputRow({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{spec.dtype === "bool" ? (
|
||||
<Select
|
||||
value={declared.initial === true ? "true" : "false"}
|
||||
onValueChange={(next) =>
|
||||
onChange({ ...declared, initial: next === "true" })
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="!h-8 flex-1 text-sm"
|
||||
aria-label="Starting value"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">true</SelectItem>
|
||||
<SelectItem value="false">false</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={asText(declared.initial)}
|
||||
placeholder="starts at"
|
||||
aria-label="Starting value"
|
||||
className="h-8 flex-1 text-sm"
|
||||
onChange={(event) =>
|
||||
onChange({
|
||||
...declared,
|
||||
initial: parseByDtype(spec.dtype, event.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<DtypeValue
|
||||
dtype={spec.dtype}
|
||||
value={declared.initial}
|
||||
label="Starting value"
|
||||
placeholder="starts at"
|
||||
className="flex-1 text-sm"
|
||||
onChange={(initial) => onChange({ ...declared, initial })}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
|
||||
Reference in New Issue
Block a user