Harden LLM cache files, diff snapshots, and approval ID indexes against truncated or malformed JSON after process termination.
Disposable cache and snapshot corruption now recovers as a miss; authoritative resume state still surfaces malformed JSON. Approval short-ID indexes use atomic no-overwrite publication and degrade to full resume-token approval when the index cannot be published durably.
Closes#111.
Closes#112.
Closes#113.
Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Fixes#108 and #109.
- Replace direct state writes with same-directory atomic temp-file writes.
- Preserve existing file modes and create new state files as 0600.
- Clean up temp files on failed replacement paths.
- Add state/SDK regression coverage plus changelog entry.
Proof:
- pnpm run typecheck
- node --test dist/test/state.test.js dist/test/resume.test.js dist/test/multi_approval_resume.test.js dist/test/approve_preview.test.js
- pnpm run lint
- pnpm run test
- built SDK write/read proof preserved 0600 across replacement
- autoreview clean: no accepted/actionable findings
Co-authored-by: Krasimir Kralev <krasi@idrobots.com>
* fix(retry): only propagate AbortError on external cancellation, not per-attempt timeout
Fixes#105.
withRetry unconditionally re-threw AbortErrors before calling shouldRetry,
causing timeout_ms + retry.max combinations to always result in a single
attempt regardless of retry configuration. Fix: check options?.signal?.aborted
before short-circuiting — external workflow cancellation still propagates
immediately, but per-attempt timeout AbortErrors now flow through shouldRetry.
* test(retry): update abort-error test to use aborted external signal; add timeout-retry unit test
Update withRetry test to properly simulate external cancellation (aborted
signal) rather than a bare AbortError without signal context.
Add unit test proving per-attempt timeout AbortErrors (no external signal)
are now retried as documented when timeout_ms + retry are combined.
* fix(retry): revert quote style to single-quote (match fork base)
* fix(retry): revert test quote style to single-quote (match fork base)
* test: add workflow-level proof that timeout_ms + retry retries on timeout
Integration test that runs a real step with timeout_ms=1500 + retry.max=3
where the command hangs past the timeout on attempts 1-2 (SIGKILLed) then
succeeds on attempt 3. Asserts status ok + attempt 3 + [RETRY] logs.
Verified the test fails against the pre-fix withRetry (short-circuit on
any AbortError) and passes with the fix. Addresses the review request for
real behavior proof at the workflow level, complementing the existing
withRetry unit tests.
* docs: credit timeout retry fix
* test: harden timeout retry workflow proof
* style: format timeout retry patch
---------
Co-authored-by: KrasimirKralev <krasi@idrobots.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Adds retry field to workflow steps with exponential/fixed backoff,
jitter, and configurable max attempts. Retry delays are signal-aware
(abort cancels immediately). Abort errors never trigger retries.
- New src/core/retry.ts: withRetry utility with backoff calculation
- Step execution wrapped in retry loop when retry.max > 1
- Retry attempts logged to stderr as [RETRY] messages
- Dry-run renders retry config (attempts, backoff, base delay)
- Comprehensive validation for all retry fields
* feat: add short approval IDs for chat-friendly resume
When a workflow or pipeline hits an approval gate in tool mode,
the response now includes an 'approvalId' (8-char hex string)
alongside the existing base64url resumeToken.
Consumers can resume using either:
lobster resume --token <base64url> (existing, still works)
lobster resume --id <8-char-hex> (new, chat-friendly)
This solves the problem of passing opaque 120+ char base64 tokens
through chat interfaces (Telegram, Discord, etc.) where they get
mangled, truncated, or are painful to copy/paste.
Implementation:
- generateApprovalId() produces 8 hex chars via crypto.randomBytes
- writeApprovalIndex() creates a small reverse-index file mapping
approvalId → stateKey in the state directory
- resolveApprovalId() looks up the index to reconstruct the token
- Kind detection (workflow-file vs pipeline-resume) uses stateKey
naming convention
- Index files are cleaned up after resume or cancellation
Fully backward-compatible: resumeToken is always present, approvalId
is additive. No breaking changes to the envelope schema.
5 new tests covering: ID presence, resume-by-ID, cancellation-by-ID,
invalid ID error, and backward compat with --token.
* fix: address review findings — orphan cleanup, multi-step gate, shared helper
Bug fixes:
- CLI multi-step pipeline now generates approvalId on second+ gates
(was only done in tool_runtime path)
- --token resume now cleans up orphaned approval index files via
cleanupApprovalIndexByStateKey (scans state dir for matching index)
Code quality:
- Extract kindFromStateKey() shared helper in resume.ts (was duplicated)
- Remove redundant dynamic import of encodeToken in tool_runtime.ts
- Sanitize approvalId in writeApprovalIndex (was only done in read/delete)
Tests:
- Add --token orphan cleanup test (verifies index gone after --token resume)
- Add double-resume test (second --id call returns clean 'not found' error)
- Total: 73/73 passing
* fix: clean up orphan approval index in tool_runtime --token path
Bug: resumeToolRequest cleaned up approval index when using --id
(via deleteApprovalId) but NOT when using --token. The CLI path
had this fix but the API path didn't — orphan index files would
accumulate in ~/.lobster/state/.
Also aligned --id/--token priority: approvalId now wins in both
CLI and API paths (was inconsistent — CLI checked approvalId first,
API checked token first via 'approvalId && !token').
73/73 tests passing.
* fix: defer approval index cleanup until after successful resume
Addresses Codex review feedback on PR #48. Previously, the approval
ID index was deleted before executing the resumed pipeline/workflow.
If execution failed with a transient error, the state was still
resumable by --token but the --id alias was already gone.
Now: index cleanup happens after successful completion or explicit
denial. On runtime_error, the index is preserved so the user can
retry with the same --id.
* fix(approval): prevent short ID remaps
* fix(approval): clean up state on alias allocation failure
---------
Co-authored-by: Sascha Kuhlmann <coolmanns@users.noreply.github.com>
* feat(run): add --dry-run flag to preview workflow execution
Add a --dry-run flag to `lobster run` that validates and prints the
execution plan without running anything. Works with both workflow files
and inline pipeline strings.
- Parses and resolves all steps, variables, and conditions
- Prints each step with resolved arguments to stderr
- Annotates approval steps as approved:true so downstream conditions
evaluate correctly
- Validates pipeline commands against the registry
- Validates stdin template refs (e.g. $step.stdout) between steps
- Annotates unresolvable output refs as [output unknown at plan time]
- Returns exit 0 on valid input, non-zero on validation errors
- Flag is consumed regardless of argv position, consistent with
--mode/--file/--args-json
Closes#45
* test(dry-run): add comprehensive test coverage
Cover shell steps, approval steps, conditional steps, inline pipelines,
exit codes, pipeline steps, stdin ref validation, unknown command
detection, flag position handling, and flag-after-file regression.
* fix(dry-run): scope --dry-run to lobster-level args only
Copilot P1: parseRunArgs was consuming --dry-run anywhere in argv,
including from unquoted pipeline stage args like:
lobster run openclaw.invoke --tool t --dry-run
Now only consumes --dry-run when no pipe character has appeared in rest
tokens yet, so:
lobster run --dry-run file.lobster (works)
lobster run file.lobster --dry-run (works)
lobster run exec | filter --dry-run (not stolen)
* fix(dry-run): keep unresolved step refs visible
* fix(dry-run): preserve known approval refs
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: coder999999999 <coder999999999@users.noreply.github.com>