Five copies of formatRelativeTime had drifted across apps/web into three
different output vocabularies, so the same timestamp read differently
depending on which surface rendered it. lib/utils.ts is now the single
implementation.
Deletes the private copies in task-item, automations/format-utils,
commit-detail-panel, and kanban/graph2-task-pipeline; commit-detail-panel
adopts the shared short form rather than keeping its long-form wording.
Adds bucket-boundary unit tests for the shared function.
Delete the two byte-identical local formatBytes copies from the chat-attachment
modules and repoint all call sites at the canonical lib/utils/format-bytes.ts,
which its own doc comment asks new code to use.
The copies had no upper clamp (>= 1 TB rendered "1 undefined") and no NaN or
negative guard (NaN rendered "NaN undefined"). Size labels now carry one
decimal ("10.0 MB" instead of "10 MB") because the canonical helper does not
strip a trailing zero; unit and e2e expectations updated to match.
PR #1810 retired the host-global gh token detection that injected GITHUB_TOKEN
into the request env, leaving both remote executors with a `_ = req` placeholder
to silence the unused parameter. resolveGHToken is now just a filter over the
selected credential IDs, so the parameter is gone from the Sprites and SSH
flavors alike. Test asserts the pass-through selection value, not just length.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(mcp): keep spawner attribution on a spawned session's first turn
A session started through spawn_session_kandev never learned who spawned
it, so it had no way to report back. The MCP handler did prepend a
<kandev-system> attribution block, but the first-turn canonicalizer
(sysprompt.canonicalizeKandevContext) drops every system block it cannot
attribute to server state — and the spawn block was not on the trusted
list, so it was deleted before the agent ever saw it. Spawns are always a
first turn, so this happened every time.
Generate the block at the launch site instead, from server-resolved
identity, and whitelist that exact content for the injector — the same
pattern entity references already use:
- config/prompts/spawned-session.md holds the attribution template.
- sysprompt.SpawnedSessionContext builds it; sysprompt.StripTags (lifted
out of the handlers' stripSystemTag) keeps embedded IDs from closing
the block early.
- LaunchSessionRequest.SpawnOrigin carries the spawner identity and is
kept off the wire (json:"-") so a WS client cannot forge a trusted
system block.
- startTask prepends the block and passes the identical content as
trusted content to both the task and Office injectors. Its trailing
env/route parameters move into a startTaskOptions struct rather than
growing the positional list again.
- The MCP handler now only fills SpawnOrigin (including the spawner's
session name) and passes the prompt through untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(mcp): derive spawn attribution from the verified spawner session
Addresses both PR review findings.
CodeRabbit: spawnOrigin copied sender_task_id / sender_session_id straight
out of the request, so an unknown or cross-task sender_session_id became
trusted launch attribution — the spawned agent would be told to report to
a session that never asked for it. Resolve the spawner session once, up
front, require it to belong to the claimed sender task, and derive the
origin (and the inherited agent profile) from that row. An unverifiable
claim now yields no attribution instead of a forged one. This also drops
a duplicate session lookup.
Codex: passthrough profiles skip the Kandev MCP wrap, so the new
attribution never reached a spawned passthrough session even though it
gets MCP configured and can reply. Give that path plain-text attribution
(no <kandev-system> markup, which would show up raw in the TTY the user
is watching). The wrap branch moves into applyLaunchPromptContext, which
keeps startTask under the nesting limit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per-session events are published on a suffixed bus subject
("shell.output.<sessionId>") but stamped with the bare event-type constant
("shell.output"). The plugin deliverer subscribed with the manifest pattern and
then re-checked that pattern against event.Type, which made every suffixed
subject undeliverable: a 3-segment pattern matched the bus subscription but
failed the re-check on segment count, while a 2-segment pattern passed the
re-check but never subscribed to the 3-segment subject.
Carry the concrete publish subject on bus.Event (stamped in place by Publish in
both the memory and NATS implementations; the NATS receive side prefers the
delivering msg.Subject) and re-check the manifest pattern against it via a new
Event.EffectiveSubject helper that falls back to Type. The concrete subject is
also what plugins receive as pluginsdk.Event.EventType -- already documented in
the proto as "bus subject" -- so a plugin subscribed to "shell.output.*" can
read the session id off it.
Audited all 109 EventBus.Publish call sites: 17 subject families were affected;
office.run.event_appended, the office tree-hold subjects and the cross-plugin
plugin.<id>.<name> already stamped Type with the full subject. Three git.ws
sites in the orchestrator publish on git.ws.<sessionId> while stamping
events.GitEvent, so the subject there is not a suffixed form of its own type at
all -- left as-is, since the gateway routes off the subject and never reads
event.Type. Unsuffixed subjects publish with subject == type, so existing
2-segment subscriptions are unaffected, and the re-check still enforces manifest
segment-count strictness over the bus's looser NATS ">" semantics.