- Session class with try/finally pattern + Client::session(callable) block helper
- Idempotent Session::close (in-memory + server idempotent)
- __destruct best-effort close fallback (errors logged via error_log, never raised)
- Client::createSession / ::session / ::listSessions / ::getSession
- TurnResult::text() helper concatenating text events
- TurnEvent value object preserves tool_call / thinking / text uniformly
- SessionState value object for list/get responses
- ClosedSessionException for use-after-close guard
- Session::__debugInfo redacts forge back-reference (no bearer in var_dump)
- tests/SessionTest.php: 15 tests covering create/close/turn/idempotency/exception/list/state/text/destruct/regression
- README "Multi-turn / Sessions (v0.2)" section + try/finally + block-form
+ __destruct + __debugInfo notes; exception tree updated
v0.1 surface unchanged — 44 existing ClientTest tests still green, +15 new
v0.2 tests in SessionTest. Client.php diff is purely additive (285 lines
appended, zero v0.1 lines touched).
Naming follows the existing PHP SDK convention: Clawdforge\Client (not the
spec's generic Sulkta\Clawdforge\Forge), with the new types living in the
same Clawdforge\ namespace and Exception\ClosedSessionException slotted
under the existing ForgeException tree.
Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
HIGH:
- H1: __debugInfo() redacts token on Client + AppToken; #[\SensitiveParameter]
on Client constructor's $token param so PHP scrubs it from stack traces.
MEDIUM:
- M1: uploadStream(StreamInterface, filename, ttl) overload so callers
handling form uploads have a non-path entry point. README warning above
the API table on uploadFile path-trust.
- M2: RunRequest now rejects empty-string model/system in the constructor
(callers should pass null/omit rather than '' to use defaults).
- M3: new MalformedResponseException extends ForgeException for
"transport succeeded, body unparseable as expected JSON object". Decoupled
from ApiException so callers can distinguish "server told me no" from
"server replied 200 with garbage". README + ApiException docstring updated.
- M4: non-UTF-8 / malformed JSON now flows through M3's new exception.
- M5: ApiException error-message extraction falls back to json_encode
(capped at 200 chars) when the error field is an object/array, so
callers don't get empty messages on {"error":{"code":...,"msg":...}}.
LOW:
- L2: revokeToken now requires server response ok === true, raises
MalformedResponseException on missing/false ok rather than silently
returning true.
- L5: README WordPress snippet uses bare Client (matches the use line above).
- L7: 29 new tests — token redaction (3), uploadStream (2), empty
model/system (2), MalformedResponseException across 7 scenarios incl.
non-UTF-8, ApiException object-error formatting + 200-char cap, revoke
ok=true requirement + ok=false + empty-name, RunRequest timeout bounds
(3) + non-string/empty files entries (2), uploadFile unreadable-path
+ 4xx + 5xx, healthz 500, Authorization header asserted on every
endpoint.
README polish: TLS verify=false caveat under "Custom HTTP client".
Audit memo: memory/clawdforge-audits/php-1cff9b8.md