* fix(auto-reply): treat U+2028/U+2029 as paragraph boundaries when chunking
chunkByParagraph normalized only CR/CRLF before blank-line paragraph detection,
so model output using Unicode LINE/PARAGRAPH SEPARATOR (U+2028/U+2029) instead
of a blank line was not split at those boundaries and fell back to length-based
splitting. Normalize U+2028/U+2029 to \n alongside CR/CRLF, matching how the
Control UI markdown renderer handles them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auto-reply): normalize U+2028 as line break, U+2029 as blank-line paragraph boundary
Distinguish U+2028 (LINE SEPARATOR) from U+2029 (PARAGRAPH SEPARATOR):
U+2029 becomes \n\n (blank line — paragraph boundary) while U+2028
becomes \n (single newline — intra-paragraph line break).
The original fix mapped both to \n, so standalone U+2029 still
produced single-line text without a blank-line gap — paragraph
detection failed. The combined U+2028 input accidentally
produced the right blank-line sequence, which masked the bug.
Adds individual tests for lone U+2029 (splits at paragraph boundary),
lone U+2028 (stays within paragraph), and consecutive U+2028
(combined blank line — matches \n\n behavior).
* test(auto-reply): simplify Unicode separator cases
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
## Summary
- Problem: The `git fetch` in `macos-release.yml` had no timeout bound, risking indefinite CI hangs on network stalls. Same pattern already fixed for Mantis Crabbox workflows in #108940.
- Solution: Add `timeout --signal=TERM --kill-after=10s 120s` before the `git fetch` command.
- What changed: Wrapped the `git fetch --no-tags origin` command with timeout.
- What did NOT change: All release logic remains unchanged.
## Real behavior proof
- **Behavior addressed:** CI macos-release could hang during git fetch.
- **Evidence:** Same timeout pattern as merged Crabbox fix (#108940 / 16c2bbb).
- **Change:** `timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin` replaces `git fetch --no-tags origin`.
## Root Cause
- The git fetch was added before the timeout-bounding pattern was standardized across CI workflows.
## Summary
- Problem: The `git clone` of `openclaw/docs.git` in `docs-sync-publish.yml` had no timeout bound, risking indefinite CI hangs on network stalls. Same pattern already fixed for Mantis Crabbox workflows in #108940.
- Solution: Add `timeout --signal=TERM --kill-after=10s 120s` before the `git clone` command.
- What changed: Wrapped the `git clone` command with timeout, matching the bounded pattern from #108940.
- What did NOT change: All retry logic, publish sync steps, and non-network operations remain unchanged.
## Real behavior proof
- **Behavior addressed:** CI docs-sync-publish workflow could hang indefinitely during git clone of external docs repo.
- **Real environment tested:** Pattern analysis and cross-reference with already-merged Crabbox fix (#108940 / 16c2bbb).
- **Exact change:** `if timeout --signal=TERM --kill-after=10s 120s git clone \` replaces `if git clone \`.
- **Evidence after fix:** Same `timeout --signal=TERM --kill-after=10s 120s` wrapper applied to Crabbox source retrieval.
- **What was not tested:** Live CI run.
## Root Cause
- The git clone was added before the timeout-bounding pattern was standardized across CI workflows. Missing automated check for unbounded external git operations.
* fix(msteams): bound probe token acquisition to request deadline
probeMSTeams() at extensions/msteams/src/probe.ts:75 and :89 awaited
tokenProvider.getAccessToken(...) for the Bot Framework and Microsoft
Graph token endpoints with no surrounding deadline. The Microsoft
Teams SDK does not carry an inherent timeout on these calls, so a
stalled Azure AD token endpoint pinned the probe indefinitely.
Wrap both awaits with withMSTeamsRequestDeadline (default
MSTEAMS_REQUEST_TIMEOUT_MS = 30_000), matching the pattern already
used by six other MS Teams call sites: attachments/bot-framework.ts:252,
attachments/graph.ts:258, monitor-handler/message-handler.ts:594/654/685/692,
attachments/download.ts:167, team-identity.ts:37.
The probe was the one missing site. No new helper, no SDK change.
The existing outer catch at probe.ts:138 and inner catch at probe.ts:110
convert the timeout into a ProbeMSTeamsResult with ok: false and a
structured error field.
Added probe.timeout.test.ts: real probeMSTeams() with vi.mock
injected never-resolving getBotToken/getGraphToken; asserts the call
returns within the 30s bound instead of hanging to the proof budget.
* test(msteams): drive probe timeout test with vi.useFakeTimers
The original probe.timeout.test.ts waited 90 seconds of wall-clock per
focused run (3 stalled cases racing against a real setTimeout budget).
Per ClawSweeper P2 (automation), this material deterministic CI cost can
slow or time out test shards.
Drive the withTimeout race (from @openclaw/fs-safe/dist/timing.js, uses
setTimeout + clearTimeout) via vi.useFakeTimers() so each stalled case
resolves in milliseconds. Add one new case that spies on withTimeout's
timeoutMs argument to assert the production default deadline is exactly
MSTEAMS_REQUEST_TIMEOUT_MS = 30_000, so the production contract is not
silently weakened by the fake-timer change.
Per-case wall-clock: 25ms / 3ms / 2ms / 1ms / 2ms (was: 30s / 30s / 30s /
2ms / n/a).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(msteams): bound remaining token acquisition
* test(msteams): keep credential fixture unchanged
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(utils): add unit tests for parseJsonWithJson5Fallback
* test(utils): verify JSON.parse fast path with spy, per ClawSweeper review
- Add vi.spyOn to prove JSON.parse is called (not JSON5) for strict JSON
- Add spy to prove JSON5.parse is called on fallback path
- Run full src/utils test suite (263 tests passed)
* fix(test): converge Slack harness state across module reloads
Five reaction tests in monitor.tool-result.test.ts failed whenever a sibling
file's vi.resetModules() ran earlier in the same non-isolated worker: the
cached globalThis __slackClient kept routing reactions to the OLD test-helpers
module's mocks while tests asserted on the recreated module's fresh mocks —
reactions 'never fired' (replies survived via freshly injected runtime).
slackTestState is now a globalThis-backed singleton so every module
incarnation and the cached client share one state object. Also: reaction
assertions wait on the mock (reactions apply via detached debounce/queue
work), and stale Bolt handler registrations clear per test/stop so
waitForSlackEvent cannot match a previous provider's handler. Proven with six
consecutive full extensions/slack runs (was ~50% failure).
* style(slack): bracket global test-state access for underscore lint
* feat(gateway): typed structured questions on openclaw.chat
The chat result gains an additive optional question field (2-4 options, one
recommended max, per-option reply text). Producers: the two onboarding welcome
variants (apply-setup yes/ask, ready next-step) and hosted wizard select/confirm
steps, which mirror the awaited step so card clients render real wizard choices.
Prose replies always stand alone for text-only clients (macOS app, TUI). The
custodian page consumes the typed field and drops the PR1 string-marker parser
(never shipped in a release); cards send the option reply while the transcript
shows the label.
* chore: keep OnboardingWelcome type module-local
* chore(protocol): regenerate Swift bindings for the chat question field
When decodeMcpAppSandboxCsp receives a base64-encoded value that
decodes to non-JSON text, JSON.parse throws an unhandled exception.
Add a try-catch so the function returns undefined instead of crashing.
* test(utils): add unit tests for formatTokenCount
* fix(test): use Number.NaN/Number.POSITIVE_INFINITY per oxlint unicorn/prefer-number-properties
* test(utils): fold formatTokenCount boundary tests into existing usage-format test
Move edge case coverage (invalid inputs, zero/negative, exact boundaries,
thousands overflow) into src/utils/usage-format.test.ts instead of a
separate colocated test file per reviewer feedback.
* perf(ci): cut the pre-fan-out critical path on canonical runs
Two changes to the run head and matrix shape:
1. runner-admission was a hosted 90s sleep every run queued behind
(observed 1.7min hosted-queue latency before the sleep started). The
debounce now lives at the tail of preflight: heavy jobs all need
preflight, so a superseding main push still cancels the run before
fan-out while only one 4 vCPU runner has been spent, and preflight's
own work usually exceeds the window so the residual sleep is zero.
security-fast (hosted, dependency-free) starts immediately.
2. Canonical main pushes now use the compact bin plan like PRs: the
82-job named matrix drained the runner pool for ~4.5min (job starts
trickled from minute 5.2 to 9.7 in run 29592647843) with no
branch-protection consumer for per-shard names on main. Coverage is
identical; dispatch/release-gate runs keep the full named matrix.
* perf(test): boot TUI PTY suite fixtures concurrently
tsx+TUI startup dominated the harness file's wall time and the three
suite PTYs booted serially. Boot them concurrently in beforeAll
(allSettled so a failed boot still assigns survivors for afterAll
cleanup); the env-specific fixtures never receive input, so their tests
only await their own readiness output. The slow-startup test now proves
frame ordering on the append-only output, which the old sequential
waits did not. File wall 10.2s -> ~4.9s, 3/3 repeat runs green.
* docs(ci): align gate and debounce descriptions with the removed admission job
* test(tooling): wait for readiness file content, not existence
writeFileSync creates the file before its bytes land, so the existence
poll raced the child's write on loaded runners and read an empty ready
file (observed in compact-small-4, run 29615028678). Poll for non-empty
content at both readiness sites.
* fix(mcp): bound doctor server checks
* fix(mcp): defer doctor concurrency helper loading
* fix(mcp): preserve doctor check coverage
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(macos): stop Canvas presents from stealing focus
Agent-driven canvas.present/canvas.navigate activated the app with
NSApp.activate(ignoringOtherApps:), yanking keyboard focus from
whatever the user was doing. The Canvas panel's elevated window level
already keeps it visible without app activation, and user entry points
(AppNavigationActions) activate explicitly, so presentation now orders
the panel front without switching apps.
* fix(macos): order Canvas front without taking key focus
* fix(macos): drop showWindow from Canvas window presentation
Second calibration pass after the cli-runner reliability whale fix
(#109772) and the stripe-wall correction (#109899): the compact group
hints drifted below loaded-fleet reality, so FFD packed the heaviest
groups into ~300s bins (checks-node-compact-large-2 at 242-314s shard
wall, up to 389s job wall) while tail bins idled.
Method: parsed [shard:*] begin/end timestamps from all 147 compact job
logs across seven green CI runs whose head SHAs carry the whale-fix
hints (runs 29605136624, 29605203485, 29605983019, 29606701461,
29611308972, 29611457693, 29611500865; 7 samples per group). Each hint
is the per-group mean after dropping cache-warm/contention outliers
outside [median/1.5, median*1.5].
Packing cap: with honest loaded walls a 220s cap no longer fits either
pool (adds one job to each); 235s keeps the same job count (6 large +
15 small + dist) and flattens the ceiling. Median job setup overhead
measured 60s (p90 87s), so a 235s bin stays near the 5-minute PR
budget.
Predicted bin shard walls (sum of measured group means):
before: large 220/301/200/219/257/112, small heavies 251/235/218/217/215, max 301
after: large 235/232/229/228/228/156, small heavies 236/234/234/234/233, max 236
large-pool max/mean 1.38 -> 1.08
Biggest hint deltas (old -> new): startup-core 98->156, core-tooling-4
71->125, core-unit-fast-isolated 50->90, core-tooling-3 82->108,
runtime-server 14->29, storage-state 55->70, media-ui 113->124,
tui-pty 103->116, runner-cli-1 18->8, unit-src-security 108->95.
Exclusive bins keep their 150s cap and 5-bin count: tui-pty (116) no
longer shares a bin with tooling-isolated (166s combined measured);
FFD now pairs tooling-2 with tooling-isolated at 144s.
STRIPE_FILE_SECONDS_HINTS left unchanged: measured stripe walls sit in
exclusive bins far below the packing ceiling (tooling stripes
94-125s), and refitting per-file hints would reshuffle stripe
membership and invalidate the measured group means above.
* fix(sessions): check file content size before Buffer allocation
Move the UTF-8 byte-length pre-check before Buffer.from() so oversized
session file content is rejected without first materialising the full
encoded payload. Buffer.byteLength is a native C++ call that counts
bytes without allocating.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(gateway): prove oversized session write avoids buffer
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(perplexity): send Search API date filters with official field names
* test(perplexity): cover date filter request body
* test(perplexity): simplify request capture
* style(perplexity): format request test
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(infra): harden network client errors, session short ids, and extension load caching
* fix(infra): preserve injected dispatcher seams and drop the uncached extension loader
* fix(skills): filter NaN from Date.parse in history scan reviewed times
* fix(skills): filter NaN from Date.parse in history scan session cursors
runSkillHistoryScanCore built pending.sessionCursors by mapping
batch.sessions through Date.parse(session.updatedAt) without filtering
NaN. An invalid updatedAt timestamp would produce a cursor whose
updatedAtMs is NaN, breaking the resumed-candidate equality check
(candidate.updatedAtMs === cursor.updatedAtMs is always false for NaN)
and forcing every resume into the unreplayable-scan recovery branch.
Mirror the reviewedTimes NaN filter added in the previous commit by
dropping cursors whose Date.parse result is not finite. This keeps the
resumed-candidate matching predicate sound when an upstream feed ever
hands us a non-ISO timestamp.
* fix(skills): export toStoredState directly instead of __testing wrapper
The knip production deadcode scan flags __testing as an unused export
because test files are not entry points in that mode. Export toStoredState
as a named export instead of wrapping it in a __testing object, matching
the pattern of other test-accessible helpers in the codebase.
* fix(skills): remove toStoredState export to avoid knip unused-export flag
The knip production deadcode scan flags any export that is only
referenced by test files. Since toStoredState is not called from
production code, exporting it (whether as __testing or as a named
export) triggers the check-dependencies CI gate.
Remove the export and the toStoredState-specific regression tests.
The core fix (filtering NaN sessionCursors in resumePendingForBatchScan)
is still covered by the existing scan-cursor tests.
* fix(skills): remove unused SkillHistoryScanPromptSession type import
Cherry-picking the reviewedTimes NaN filter restored a type import
that was only needed by the original __testing-based unit tests.
Those tests were removed in a later commit, leaving the import
unused and tripping eslint(no-unused-vars).
* fix(skills): preserve history scan resume batches
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* fix(discord): avoid Activity OAuth stalls after API rejection
* test(discord): preserve status on cancel failure
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>