Before, /new and /clear were identical: both just dropped the local
conversationId->sessionId mapping. The agent-side session was never
disposed (opencode sessions leaked), and the two commands had no real
difference beyond their ack text.
Align them to each channel's real capability:
- /new (resetSession): drop the local mapping only; the old agent session
is left intact and stays resumable where the agent supports it.
- /clear (new sessionClearer): actively dispose the current session via the
agent's real delete primitive. opencode implements it with
DELETE /session/:id (idempotent on 404). Channels whose agent exposes no
delete in the mode DWS drives it (Codex app-server, Qoder stream, Claude
exec) fall back to resetSession, so /clear behaves like /new there.
Tests pin both directions: /clear issues exactly one DELETE and drops the
mapping; /new (resetSession) issues zero DELETEs so the old session stays
resumable. Doc updated to describe the per-channel behavior.
go build ./... ok; go vet ok; go test ./internal/helpers ok.
The shared opencode http.Client had a hardcoded 30s Timeout used for every
request including POST /session/{id}/message. Long agent turns (e.g. a web
research report) take longer than 30s, so the client aborted mid-flight with
"context deadline exceeded (Client.Timeout exceeded while awaiting headers)",
even though the per-turn budget (DWS_AGENT_TIMEOUT_MS, default 300s) was far
larger. Retrying hit the same wall.
Drop the client-level deadline (rely on the per-request ctx that doJSON already
wires via http.NewRequestWithContext) and bound only the /global/health probe
with a short 10s timeout so startup detection stays snappy.
Tests: pin Timeout==0 on the default client, and assert a slow reply succeeds
within the turn budget but is cut when the turn ctx is shorter than the reply.
PR #448 stripped a leading H1 matching --name on the markdown path of
`dws doc create`, but the JSONML path (--content-format jsonml) was never
covered. Rich documents — tables, callouts, styled blocks — go through
create_document + update_document(jsonml=...), which writes the body
verbatim, so a leading h1 whose text equals the document name renders the
title twice (the "two headings" effect the doc platform shows because it
already renders --name as the page title).
Add stripLeadingDuplicateTitleJSONML: parse the marshaled JSONML body,
skip an optional ["root", {}, ...] wrapper, and drop the first node when
it is an h1 whose concatenated leaf text equals --name (trimmed,
case-insensitive). Any parse failure or non-match leaves the body
untouched, so a valid write is never blocked. A stderr note mirrors the
markdown path so agents learn the convention.
go:embed the skills/ tree (mono + multi) into the binary and make
`dws skill setup` install from the embedded bundle by default. Upgrading
the binary now refreshes the installed skill, instead of silently reusing
a stale copy probed from the current working directory. An explicit
--source / DWS_SKILL_SOURCE still overrides for development.
Replants the change from #441 onto main: #441 was stacked on the
abandoned feat/chat-bot-provisioning-openclaw base (PRs #407/#397 closed),
which is why its CI was red. The embedded-bundle test asserted a
connect.md doc that only existed on that dead base; retargeted it to a
reference doc that exists on main.
When `dev connect` runs with no credentials (no --robot-client-id/secret, no
--unified-app-id) in a real terminal, it now guides the user instead of failing:
- Ask "new app or existing app?".
- Existing: read a unified-app-id (reuses the credentials-get path) or an
explicit clientId/clientSecret pair.
- New: read the app name, robot display name and description, submit the async
robot-create task, then poll for the result to obtain credentials. This has a
real side effect (it provisions a real robot app).
connect_onboarding.go keeps the flow channel-agnostic and io-injected so the
whole decision tree is unit-tested with a mock runner and scripted stdin (no
network, no real provisioning). Non-interactive invocations — scripts, daemons,
pipes, and --dry-run — are unchanged: they still require an explicit credential
flag, so nothing that worked before starts prompting.
Tests cover: existing/unified, existing/raw-creds, new-app submit+poll (asserts
the submit_robot_create_task / query_robot_create_result calls and params),
invalid choice, and a missing required field (no side-effecting call).
Note: the real provisioning path shares the same unverified credential field
names as devAppFetchCredentials (clientId/appKey, clientSecret/appSecret — see
its TODO(verify)); the live create→credentials flow is to be verified on the
pre-prod gateway with the -dws-devapp prerelease before relying on it.
opencode persists its own sessions but, unlike the Claude family, will not let
the caller mint the session id up front: `opencode run` creates a session on the
first turn and only reports its id in the `--format json` event stream, then
continues it with `--session <id>`. So the connector now CAPTURES the id from
the output (like codex's thread id) instead of minting a UUID:
- connect_opencode.go: a dedicated opencodeForwarder that runs
`opencode run --pure --format json`, accumulates reply text from `text`
events, captures the top-level sessionID, and on the first turn persists the
convID->sessionID map to <config>/connect/<clientId>/opencode-sessions.json
(best-effort, restored on startup). The next message — and a connector
restart — replays it with `--session <id>`. Implements streamingForwarder and
sessionResetter, so /new and /clear work and streaming cards stream.
- forwarderForChannel routes the opencode channel to it.
- connectAgentOptionsPayload reports opencode memory as
"per-conversation-captured"; --agent-memory help now lists codex/opencode too.
Verified end to end against the real opencode CLI: turn 1 captures the session
id, turn 2 with --session recalls a fact from turn 1. Unit tests cover capture +
resume (stub bin), store persistence across restart, and reset.
gemini also exposes --session-id/--resume but could not be verified end to end
here (no GEMINI_API_KEY) and its --resume id semantics are unconfirmed, so it is
left for a follow-up rather than wired blind. qoder's qodercli still has only
--resume (no addressable id) and cannot do per-conversation memory.
Two connector improvements for the linked-bot chat experience, modelled on
codex / OpenClaw connect:
1. Built-in slash commands. A user can type /new (aliases /start, /reset) to
open a fresh session or /clear to wipe the current one, instead of the
message being forwarded to the agent. The command set is fixed (never
dynamically extended) so behaviour stays predictable. Matching requires the
whole message to be the bare command, so a normal question that merely starts
with a slash is forwarded untouched. Costs no agent turn / tokens.
- connect_command.go: channel-agnostic parser + action table (+ tests).
- sessionResetter optional interface; execForwarder (Claude family) and the
codex app-server forwarder both implement it. The main loop intercepts a
command right after the owner-decision interceptor and acks via webhook.
2. Persist codex per-conversation context. codexThreadSessions previously kept
the convID->threadID map in memory only, so a codex connector restart lost
every conversation's context — unlike the Claude-family convSessions store
which already persisted. The thread identity is now the authoritative,
mutex-guarded map persisted to <config>/connect/<clientId>/codex-threads.json
(best-effort, atomic write), restored on startup. An empty clientId keeps it
in memory only, preserving the original behaviour. This also removes a data
race on threadID by moving it off the per-conversation state into the
sessions store.
The opencode channel forwarded to `opencode run` without isolating the
operator's interactive environment. claudecode already neutralises via
`--setting-sources project --strict-mcp-config`; opencode had no equivalent.
When the operator runs a plugin suite that swaps the global default agent at
runtime (e.g. OhMyOpenCode's rotating Greek-codename agents), headless
`opencode run` crashes resolving a default agent that no longer exists
("default agent ... not found"), and the crash trace is sent back to DingTalk
as the bot reply. Verified by reproducing in a clean /tmp dir: plain
`opencode run` crashes, `opencode run --pure` replies normally.
Add --pure to the opencode argvTail so the bot answers in a clean,
plugin-free environment, consistent with the claudecode channel.
Previously every user-identity send/reply attached clawType=edition.ClawType()
unconditionally, so the IM server rendered an AI-sent badge ("通过AI发送" on the
open edition) under every message — surprising users by branding all their sends.
Make it opt-in: by default no clawType is attached (no badge). Passing --ai-tag
on 'chat message send' / 'chat message reply' attaches edition.ClawType() so the
badge shows (open=openClaw -> 通过AI发送, wukong overlay -> 悟空AI发送). Bot and
webhook sends remain untouched.
Follow-up to #475 (which fixed the hardcoded wukong leak, #474).
Attach the clawType tool argument to every user-identity send path
(send_personal_message text/rich-media/reply and
send_direct_message_as_user) so the IM server can render the
"Send from AI" indicator on delivered messages. The value comes from a
new edition hook (Hooks.ClawTypeValue, exposed via edition.ClawType())
that falls back to DefaultOSSClawType ("openClaw"); overlays such as
wukong set their own identity to get their branded indicator.
Also fixes the reply command, which hardcoded clawType="wukong" and
made open-source replies carry the Wukong AI identity.
Bot sends intentionally stay untouched: they already render as bot
messages and must not carry the user-identity claw tag.