Skip to content

🤖 refactor: cut the taskService/workspaceService dependency cycle at a typed seam - #3996

Merged
ibetitsmike merged 20 commits into
mainfrom
mike/task-workspace-seam
Aug 29, 2026
Merged

🤖 refactor: cut the taskService/workspaceService dependency cycle at a typed seam#3996
ibetitsmike merged 20 commits into
mainfrom
mike/task-workspace-seam

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Cuts the TaskService/WorkspaceService concrete-class dependency cycle with one-directional typed ports, then pays for the new contract by deleting the code and test scaffolding the ports make obsolete. The full branch is a net simplification: +676/−1010 (net −334) across 11 files.

Background

TaskService and WorkspaceService imported each other's concrete classes, so neither side had a stated contract: every public member of each class was implicitly reachable from the other, and tests smuggled partial fakes through as unknown as casts. An architecture change must shrink the codebase, so the seam had to delete more than it declares.

Implementation

  • taskWorkspaceSeam.ts is the single home for the two ports and every type they share:
    • WorkspaceHost (~38 methods): what TaskService may call; WorkspaceService implements it.
    • AgentTaskIntegration (12 methods): what WorkspaceService may call; TaskService implements it, wired via setAgentTaskIntegration (the old setTaskService is deleted).
    • ArchiveWorkspaceOptions, SendMessageInternalOptions (formerly a 56-line inline literal), WorkspaceLiveActivity, AgentTaskStatus, and the archive untracked-path helpers moved here instead of being duplicated; StreamErrorRecoveryOutcome is imported from its canonical agentSession export. The AgentTaskStatus re-export shim in taskService is gone; importers use the seam.
  • Host call placement is byte-position-identical to main. The call sites are race-hardened sink rechecks and admission-gate interleavings, so placement is behavior; this PR only changes what the callers are allowed to know about each other.
  • Invariants moved into types: method-level optional chaining on both ports (integration?.method?.()) and the "test harnesses mock a narrow surface" hedges are deleted because the interfaces now guarantee the methods exist.

Test shrink (where the net-negative lives)

  • Typed fakes replace all as unknown as TaskService/WorkspaceService casts: makeAgentTaskIntegrationFake() (shared testUtils) and a satisfies WorkspaceHost central factory.
  • Deleted tests the port makes unwritable or that only asserted wiring: a phantom cleanupReportedDescendantsAfterArchive guard (the method never existed in production), an archive lock pass-through assertion, and four private updateAgentStatus non-invocation spies whose positive behavior tests remain.
  • Near-identical siblings collapsed into test.each tables (send/resume interrupted-task lifecycle, winding-down gate, auto-resume reset, foreground-wait backgrounding); every case is preserved.
  • createWorkspaceServiceMocks lost its dead stubs (waitForIdle, deleteWorktree, updateAgentStatus never existed on the port), its hand-written 37-line return annotation, and unconsumed return entries; 23 copy-pasted createWorkspace mock blocks collapsed into two shared helpers typed via Parameters<WorkspaceHost["create"]>; redundant conditional-spread forwarding in two harnesses became direct pass-through.

Validation

  • make static-check green; typecheck is the enforcement mechanism for the seam (it caught two false "unused symbol" audit claims during development).
  • Full taskService/workspaceService/heartbeatService suites: 1022 tests, failures limited to the 4 pre-existing flakes reproduced identically on untouched trunk in a clean worktree.
  • Structural greps: zero remaining cross-references between the two services; the seam imports neither.

Risks

The production diff is receiver-renames, type moves, and deleted dead plumbing; runtime control flow is unchanged (one .bind indirection replaced by a direct guarded call). Regression risk concentrates in the test consolidation: a test.each translation error could weaken a race guard. Mitigated by preserving each original case row and assertion set, and by the suites passing with an unchanged test count on the task side.


Generated with xum • Model: anthropic:claude-fable-5 • Thinking: xhigh

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: a6bf1b2a4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

@ibetitsmike
ibetitsmike force-pushed the mike/task-workspace-seam branch from a6bf1b2 to 3f94aa7 Compare August 28, 2026 08:27
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 3f94aa704c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Branch reworked per reviewer directive: the seam now pays for itself. Net diff vs main is +676/−1010 (−334). New commits dedupe seam types into taskWorkspaceSeam.ts, encode port invariants in types instead of optional chaining, and delete seam-obsoleted test scaffolding (phantom-method guard, wiring-only tests, private-spy tests, copy-pasted mock blocks, dead stubs).

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 122fb5bf0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

_Generated with `xum` • Model: `openai:gpt-5.6-sol` • Thinking: `high`_

<!-- mux-attribution: model=openai:gpt-5.6-sol thinking=high -->
Review follow-up: the four per-command handlers are only reachable through
processSlashCommand now, and /plan open lost its dedicated tests in the
result-based rewrite.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P1: getDraft captured at command invocation reports that render's
input, so async commands cleared newer drafts on consume and never fired
restore-if-empty.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P2: text equality cannot distinguish a retyped identical draft from
the original invocation. Commands already clear through their own
clear-input actions (matching trunk), so the terminal clear was additive
and could only destroy mid-phase drafts.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
Codex P2 follow-up: with the terminal consume-path clear gone, /dream and
/refine left the executed command re-runnable in the composer. Emit
clear-input from the handlers so commands own their composer effects.

_Generated with `xum` • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh -->
The seam file is now the single documented home for ArchiveWorkspaceOptions,
SendMessageInternalOptions, and WorkspaceLiveActivity instead of duplicating
workspaceService declarations. StreamErrorRecoveryOutcome comes from its
canonical agentSession export. The AgentTaskStatus re-export shim is gone;
importers use the seam. Method-level optional chaining on both typed ports is
removed: the interfaces guarantee the methods, so the narrow-mock hedges and
their comments no longer apply.
… suite

The typed AgentTaskIntegration port makes several tests unwritable or
redundant: the phantom cleanupReportedDescendantsAfterArchive guard (method
never existed in production), the archive lock pass-through wiring assertion,
and four private updateAgentStatus non-invocation spies whose positive
registerSession behavior tests remain. Near-identical send/resume lifecycle,
winding-down, auto-resume, and foreground-wait-backgrounding siblings collapse
into table-driven tests preserving every case, and dead fake stubs the code
under test never reads are dropped.
…d host port

createWorkspaceServiceMocks loses its dead stubs (waitForIdle, deleteWorktree,
updateAgentStatus never existed on WorkspaceHost), its hand-written 37-line
return annotation, and return entries nothing consumes; the
removeQueuedMessagesByDedupeKeyPrefix override is wired instead of the one
call site mutating the built host. 23 copy-pasted createWorkspace mock blocks
collapse into two shared helpers, and the redundant conditional-spread
forwarding in both harnesses becomes direct pass-through since the factory
already defaults absent overrides.
Drops fake overrides that restate makeAgentTaskIntegrationFake defaults,
types makeWorkspaceTurnCreateMock's rest args via Parameters<WorkspaceHost['create']>
instead of an unknown[] cast, and calls withTaskTreeLifecycleLock directly
rather than through a bound closure.
@ibetitsmike
ibetitsmike force-pushed the mike/task-workspace-seam branch from 122fb5b to e05dc05 Compare August 28, 2026 19:22
@ibetitsmike
ibetitsmike changed the base branch from main to mike/deep-tool-modules August 28, 2026 19:22
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 29, 2026
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was unmerged and was not in the queue Aug 29, 2026
Base automatically changed from mike/deep-tool-modules to main August 29, 2026 05:45
@ibetitsmike
ibetitsmike added this pull request to the merge queue Aug 29, 2026
Merged via the queue into main with commit 3dfccfa Aug 29, 2026
53 of 74 checks passed
@ibetitsmike
ibetitsmike deleted the mike/task-workspace-seam branch August 29, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant