Snapshot registered commands, then deactivate state immediately on abort.
Prevents race where new monitor activates fresh state that gets wiped
by the delayed .then() of the old monitor's cleanup promise.
- Export listSkillCommandsForAgents and SkillCommandSpec from plugin-sdk/index.ts
(removes deep relative import in monitor.ts)
- Add originalName field to MattermostCommandSpec for preserving pre-prefix names
- Build trigger→originalName map in monitor.ts, thread through slash-state → slash-http
- resolveCommandText() now uses triggerMap for accurate name lookup
(oc_report → /oc_report correctly, not /report)
- normalizeAllowList/isSenderAllowed in slash-http.ts now matches the
websocket monitor: strips mattermost:/user:/@ prefixes and supports
the '*' wildcard, so configs that work for WS also work for slash cmds
- registerSlashCommandRoute extracts pathname from explicit callbackUrl
and registers it alongside callbackPath, so callbacks hit a registered
route even when callbackUrl uses a non-default pathname
Addresses Codex review round 5 (P1 + P2).
- Reject slash command registration when callbackUrl resolves to
loopback but Mattermost baseUrl is remote; log actionable error
directing user to set commands.callbackUrl explicitly
- Honor commands.nativeSkills: when enabled, dynamically list skill
commands and register them with oc_ prefix alongside built-in commands
- Reconcile existing commands on callback URL change: attempt PUT update,
fallback to delete+recreate for stale commands with mismatched URLs
- Add updateMattermostCommand() for PUT /api/v4/commands/{id}
Addresses Codex review round 4 (P1 + P2 items).
- parseSlashCommandPayload JSON branch now validates required fields
(token, team_id, channel_id, user_id, command) like the form-encoded
branch, preventing runtime exceptions on malformed JSON payloads
- normalizeCallbackPath() ensures leading '/' to prevent malformed URLs
like 'http://host:portapi/...' when callbackPath lacks a leading slash
- Applied in resolveSlashCommandConfig and resolveCallbackUrl
Addresses Codex review round 3 (P2 items).
Address Codex review findings:
1. slash-http.ts: Token validation now rejects when commandTokens set is
empty (e.g. registration failure). Previously an empty set meant any
token was accepted — fail-open vulnerability.
2. slash-state.ts: Replaced global singleton with per-account state Map
keyed by accountId. Multi-account deployments no longer overwrite each
other's tokens, registered commands, or handlers. The HTTP route
dispatcher matches inbound tokens to the correct account.
3. monitor.ts: Updated getSlashCommandState/deactivateSlashCommands calls
to pass accountId.
Register custom slash commands via Mattermost REST API at startup,
handle callbacks via HTTP endpoint on the gateway, and clean up
commands on shutdown.
- New modules: slash-commands.ts (API + registration), slash-http.ts
(callback handler), slash-state.ts (shared state bridge)
- Config schema extended with commands.{native,nativeSkills,callbackPath,callbackUrl}
- Uses oc_ prefix for triggers (oc_status, oc_model, etc.) to avoid
conflicts with Mattermost built-in commands
- Opt-in via channels.mattermost.commands.native: true
- Capability nativeCommands: true exposed for command registry
Closesopenclaw/openclaw#16515
fix: improve compaction summary instructions to preserve active work
Expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context.
Co-authored-by: joetomasone <56984887+joetomasone@users.noreply.github.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>
Complete the stop reason propagation chain so ACP clients can
distinguish end_turn from max_tokens:
- server-chat.ts: emitChatFinal accepts optional stopReason param,
includes it in the final payload, reads it from lifecycle event data
- translator.ts: read stopReason from the final payload instead of
hardcoding end_turn
Chain: LLM API → run.ts (meta.stopReason) → agent.ts (lifecycle event)
→ server-chat.ts (final payload) → ACP translator (PromptResponse)
* fix(gateway): flush throttled delta before emitChatFinal
The 150ms throttle in emitChatDelta can suppress the last text chunk
before emitChatFinal fires, causing streaming clients (e.g. ACP) to
receive truncated responses. The final event carries the complete text,
but clients that build responses incrementally from deltas miss the
tail end.
Flush one last unthrottled delta with the complete buffered text
immediately before sending the final event. This ensures all streaming
consumers have the full response without needing to reconcile deltas
against the final payload.
* fix(gateway): avoid duplicate delta flush when buffer unchanged
Track the text length at the time of the last broadcast. The flush in
emitChatFinal now only sends a delta if the buffer has grown since the
last broadcast, preventing duplicate sends when the final delta passed
the 150ms throttle and was already broadcast.
* fix(gateway): honor heartbeat suppression in final delta flush
* test(gateway): add final delta flush and dedupe coverage
* fix(gateway): skip final flush for silent lead fragments
* docs(changelog): note gateway final-delta flush fix credits
---------
Co-authored-by: Jonathan Taylor <visionik@pobox.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>