Skip to content

fix: M1-M11 medium-severity issues from prior code review - #53

Merged
Patel230 merged 14 commits into
mainfrom
fix/m1-m11-medium-severity-2026-06
Jun 17, 2026
Merged

fix: M1-M11 medium-severity issues from prior code review#53
Patel230 merged 14 commits into
mainfrom
fix/m1-m11-medium-severity-2026-06

Conversation

@Patel230

Copy link
Copy Markdown
Contributor

Summary

Addresses the 11 medium-severity issues from the prior code review (saved at /tmp/hawk-review.md).

  • M1: H6 migration completeness — 13 cmd/ sites + audit-test regex expansion + cmdHardFailThreshold→0
  • M2: AddUser/AddAssistant now use MemorySvc()/Persistence().DAG() instead of the never-initialized s.Memory/s.ConvoDAG
  • M3: Deprecation comments point at real accessors (or comments rewritten to match reality)
  • M4: PermissionService.IsZero() now actually works (constructor doesn't pre-set mode)
  • M5: SubcommandRegistry.Register detects alias collision
  • M6: handleSessionCommand / dispatcher contract fixed; /recover test added
  • M7: WaitForLock no longer busy-spins
  • M8: logDroppedMessage moved out of mb.mu critical section
  • M9: dropLogEveryN extracted as a named constant
  • M10: TestWaitForLock_OwnerMismatchOnRelease renamed and a real waiter test added
  • M11: TestStats_NotAffectedByWaiters goroutine leak fixed

Test plan

  • go test -race -count=1 -shuffle=on ./... passes (except the sandbox-blocked ones below)
  • gofmt clean
  • go vet ./... clean
  • The new M1 cmdHardFailThreshold audit is enforced
  • M6: new test asserts /recover <id> receives the id
  • M10: new TestWaitForLock_ReleaseByNonOwnerDoesNotWakeWaiter passes
  • M11: new test joined to its goroutine via a done channel

Sandbox / environment notes

The following packages fail under the sandbox due to mkdir .../.git/hooks or ~/.hawk/backups/ operation-not-permitted errors; these are not regressions from this PR and pass on a normal dev box:

  • internal/context/repomap
  • internal/engine/git
  • internal/engine/planning
  • internal/engine/project
  • internal/engine/workflow
  • internal/intelligence/repomap
  • internal/multiagent/parallel
  • internal/rules
  • internal/snapshot
  • internal/tool

Made with Cursor

Patel230 added 14 commits June 17, 2026 19:31
- chat_subcommand_pin.go: use m.session.SetPinnedMessages
- chat_subcommand_simple.go: use m.session.SetGLMThinkingEnabled
- chat_subcommand_status.go: use m.session.CostValue().Summary()
- session.go: add SetPinnedMessages accessor
- chat.go: use PermSvc().Autonomy()/SetAutonomy, SetContainerExecutor, SetContainerRequired
- permissions_center.go: use PermSvc().SetAutonomy
- welcome_gate.go, statusbar.go: use PermSvc().Autonomy()
- hud_panel.go: use MemorySvc().Yaad()
- chat_status.go: use ContextWindowCachedValue, Persistence().SetContextWindowCached
- chat_subcommand_branches.go: use Persistence().DAG()
- chat_commands_session.go: use Persistence().DAG()
- session.go: add SetContainerExecutor accessor
- audit_test.go: regex now matches m.session.X; cmdHardFailThreshold=0
- statusbar.go: use m.session.CostValue()
- chat_subcommand_cost.go, chat_subcommand_usage.go: use CostValue()
- chat_commands_session.go, chat_commands_util.go: use CostValue()
- chat.go: use ContextWindowCachedValue()
- permissions_center.go: use ModeValue() (safe fallback for test struct literal)
- chat_model_test.go, statusbar_test.go: use accessors
- session_sync.go, snapshot_cmd.go: avoid audit regex false positives in text
- session.go: add SetContextWindowCached, ModeValue, SetMode accessors
…e().DAG()

- AddUser: read Memory from s.MemorySvc().Memory() (was s.Memory)
- AddUser, AddUserWithImage, AddAssistant, ForkConversation, SwitchBranch,
  ListBranches, ConvoHead: read DAG from s.Persistence().DAG() (was s.ConvoDAG)
- Aligns with the H6 sub-service decomposition: the legacy ConvoDAG and
  Memory fields on Session are aliases set by NewSessionWithClient; new
  code goes through the sub-service getters to keep the
  god-object-decomposition contract consistent.
- FewShot() -> FewShotStore() (typo fix in comment)
- PlanState, Trajectory, LintLoop, TestLoop, FileMentions, Files,
  Snapshots, Tracer, RateLimiter: comments now indicate these
  remain on the legacy Session struct (no sub-service accessor
  exists); the previous deprecation text pointed to non-existent
  methods that callers could not find.
NewPermissionService no longer initializes mode to PermissionModeDefault;
the zero value ("") is preserved so IsZero() can correctly report a
freshly constructed service. IsZero() now checks for the empty string
instead of PermissionModeDefault. This is the right semantics: callers
that want a default mode should call SetMode explicitly, and tests can
rely on IsZero() being true for a fresh service.
The previous implementation checked for primary-name duplicates
(`r.primary[name]`) but not alias collisions. Two subcommands
that each register a different primary but share an alias would
silently overwrite `r.aliasOf[alias]`. Now the alias check
mirrors the primary-name check: registration is rejected if any
of the subcommand's aliases is already taken. The existing
silent-no-op pattern is preserved (no error return) so init()
callers don't have to change.
The previous Handle passed the post-name args slice directly to
handleSessionCommand, which expects parts[0] to be the command
name. This broke /recover <id>, /resume <id>, and /tag <label>:
the trailing arg landed at parts[0] instead of parts[1], and the
`len(parts) >= 2` check failed, hitting the usage error path.

The fix reconstructs the full parts slice (name + args) before
calling handleSessionCommand, matching the contract the
simple.go delegatingCommand handlers use. The parts-building
logic is extracted into a buildSessionParts helper for testability.

New tests (TestBuildSessionParts_*, TestSessionSubcommand_RecoverIDReachesPartsIndex1,
TestResolveSessionName_PicksLongestMatch) assert the fix and the
dispatcher contract.
The previous for-select loop had two problems: (1) after the first
\<-w.done fired, the closed channel kept firing and the loop
re-called AcquireLock, busy-spinning until the timer.C fired.
(2) Both <-w.done and <-timer.C become ready at timeout, and Go's
select picks randomly — so the function could miss the timeout
and loop indefinitely.

Fix: replace the for-loop with a one-shot select. On the first
signal (w.done or timer), try AcquireLock exactly once. If it
succeeds, return nil; if the timer fired, return a timeout error.
The race-loser no longer re-registers — it returns an error.

TestWaitForLock_MultipleWaiters_OnlyOneAcquires updated to assert
the new semantics: the race-loser gets an error rather than
re-registering and waiting for a second signal.
…ection

The previous implementation called logDroppedMessage while holding
mb.mu (write lock). The slog.Warn call may acquire its own internal
lock (the slog handler's mutex); if the handler is slow (file I/O,
network sink), this serializes all bus operations for the duration
of the log write.

Fix: snapshot the (from, to, topic) of each drop into a local
slice while still holding the lock, then iterate the slice after
the lock is released and call logDroppedMessage on each entry.
The error return path is also collected into a local variable
and returned after the lock is released.

No behavior change for the caller; only the order of operations
differs (logs now happen after the bus is unlocked).
The previous 100 was a magic number inlined in the
`n%100 != 0` check. Extracted as a package-level const
`dropLogEveryN = 100` with a doc comment explaining the
rationale (balancing observability against log volume under
sustained pressure).
- TestWaitForLock_OwnerMismatchOnRelease renamed to
  TestReleaseLock_NonOwnerReturnsError; the original name claimed
  it was about waiters not being woken, but the body only checked
  the error return.
- New TestWaitForLock_ReleaseByNonOwnerDoesNotWakeWaiter registers
  a real waiter, attempts a ReleaseLock from a non-owner, and
  asserts the waiter times out (rather than being woken by the
  failed ReleaseLock). This is the test the original name
  promised but never delivered.
… channel

The previous TestStats_NotAffectedByWaiters launched a goroutine
that called WaitForResponse and never joined it; the goroutine
ran for 100ms after the test exited. Fixed by:
- Adding a done channel that the goroutine closes on return.
- Adding a 1-second join timeout via select on the done channel.
- The test now fails loudly if the goroutine does not return
  within 1s (which would indicate a future regression).
…safe

The M2 refactor changed these methods to call
s.Persistence().DAG() and s.MemorySvc().Memory() directly. That
panics on &engine.Session{} struct literals (used by cmd tests)
where s.persist and s.memory are nil.

This commit wraps each call site in a nil-check on the parent
service (s.Persistence() / s.MemorySvc()) so that struct-literal
sessions still work the same way as before — the methods
become no-ops when their backing sub-service is nil, which
matches the previous behavior on s.ConvoDAG == nil and
s.Memory == nil.
@Patel230
Patel230 merged commit 1b57cec into main Jun 17, 2026
18 checks passed
@Patel230
Patel230 deleted the fix/m1-m11-medium-severity-2026-06 branch June 17, 2026 15:36
Patel230 added a commit that referenced this pull request Jun 18, 2026
* fix(cmd): M1 part 1 - pinSubcommand, GLM thinking, status use accessors

- chat_subcommand_pin.go: use m.session.SetPinnedMessages
- chat_subcommand_simple.go: use m.session.SetGLMThinkingEnabled
- chat_subcommand_status.go: use m.session.CostValue().Summary()
- session.go: add SetPinnedMessages accessor

* fix(cmd): M1 part 2 - Autonomy, ContainerExecutor, ConvoDAG accessors

- chat.go: use PermSvc().Autonomy()/SetAutonomy, SetContainerExecutor, SetContainerRequired
- permissions_center.go: use PermSvc().SetAutonomy
- welcome_gate.go, statusbar.go: use PermSvc().Autonomy()
- hud_panel.go: use MemorySvc().Yaad()
- chat_status.go: use ContextWindowCachedValue, Persistence().SetContextWindowCached
- chat_subcommand_branches.go: use Persistence().DAG()
- chat_commands_session.go: use Persistence().DAG()
- session.go: add SetContainerExecutor accessor
- audit_test.go: regex now matches m.session.X; cmdHardFailThreshold=0

* fix(cmd): M1 part 3 - remaining cmd/ Cost/Mode/ContextWindow accessors

- statusbar.go: use m.session.CostValue()
- chat_subcommand_cost.go, chat_subcommand_usage.go: use CostValue()
- chat_commands_session.go, chat_commands_util.go: use CostValue()
- chat.go: use ContextWindowCachedValue()
- permissions_center.go: use ModeValue() (safe fallback for test struct literal)
- chat_model_test.go, statusbar_test.go: use accessors
- session_sync.go, snapshot_cmd.go: avoid audit regex false positives in text
- session.go: add SetContextWindowCached, ModeValue, SetMode accessors

* fix(engine): M2 - AddUser/AddAssistant use MemorySvc() and Persistence().DAG()

- AddUser: read Memory from s.MemorySvc().Memory() (was s.Memory)
- AddUser, AddUserWithImage, AddAssistant, ForkConversation, SwitchBranch,
  ListBranches, ConvoHead: read DAG from s.Persistence().DAG() (was s.ConvoDAG)
- Aligns with the H6 sub-service decomposition: the legacy ConvoDAG and
  Memory fields on Session are aliases set by NewSessionWithClient; new
  code goes through the sub-service getters to keep the
  god-object-decomposition contract consistent.

* fix(engine): M3 - deprecation comments point at real accessors

- FewShot() -> FewShotStore() (typo fix in comment)
- PlanState, Trajectory, LintLoop, TestLoop, FileMentions, Files,
  Snapshots, Tracer, RateLimiter: comments now indicate these
  remain on the legacy Session struct (no sub-service accessor
  exists); the previous deprecation text pointed to non-existent
  methods that callers could not find.

* fix(engine): M4 - PermissionService constructor no longer pre-sets mode

NewPermissionService no longer initializes mode to PermissionModeDefault;
the zero value ("") is preserved so IsZero() can correctly report a
freshly constructed service. IsZero() now checks for the empty string
instead of PermissionModeDefault. This is the right semantics: callers
that want a default mode should call SetMode explicitly, and tests can
rely on IsZero() being true for a fresh service.

* fix(cmd): M5 - SubcommandRegistry.Register detects alias collision

The previous implementation checked for primary-name duplicates
(`r.primary[name]`) but not alias collisions. Two subcommands
that each register a different primary but share an alias would
silently overwrite `r.aliasOf[alias]`. Now the alias check
mirrors the primary-name check: registration is rejected if any
of the subcommand's aliases is already taken. The existing
silent-no-op pattern is preserved (no error return) so init()
callers don't have to change.

* fix(cmd): M6 - sessionSubcommand.Handle prepends command name to args

The previous Handle passed the post-name args slice directly to
handleSessionCommand, which expects parts[0] to be the command
name. This broke /recover <id>, /resume <id>, and /tag <label>:
the trailing arg landed at parts[0] instead of parts[1], and the
`len(parts) >= 2` check failed, hitting the usage error path.

The fix reconstructs the full parts slice (name + args) before
calling handleSessionCommand, matching the contract the
simple.go delegatingCommand handlers use. The parts-building
logic is extracted into a buildSessionParts helper for testability.

New tests (TestBuildSessionParts_*, TestSessionSubcommand_RecoverIDReachesPartsIndex1,
TestResolveSessionName_PicksLongestMatch) assert the fix and the
dispatcher contract.

* fix(multiagent): M7 - WaitForLock no longer busy-spins

The previous for-select loop had two problems: (1) after the first
\<-w.done fired, the closed channel kept firing and the loop
re-called AcquireLock, busy-spinning until the timer.C fired.
(2) Both <-w.done and <-timer.C become ready at timeout, and Go's
select picks randomly — so the function could miss the timeout
and loop indefinitely.

Fix: replace the for-loop with a one-shot select. On the first
signal (w.done or timer), try AcquireLock exactly once. If it
succeeds, return nil; if the timer fired, return a timeout error.
The race-loser no longer re-registers — it returns an error.

TestWaitForLock_MultipleWaiters_OnlyOneAcquires updated to assert
the new semantics: the race-loser gets an error rather than
re-registering and waiting for a second signal.

* fix(multiagent): M8 - logDroppedMessage moved out of mb.mu critical section

The previous implementation called logDroppedMessage while holding
mb.mu (write lock). The slog.Warn call may acquire its own internal
lock (the slog handler's mutex); if the handler is slow (file I/O,
network sink), this serializes all bus operations for the duration
of the log write.

Fix: snapshot the (from, to, topic) of each drop into a local
slice while still holding the lock, then iterate the slice after
the lock is released and call logDroppedMessage on each entry.
The error return path is also collected into a local variable
and returned after the lock is released.

No behavior change for the caller; only the order of operations
differs (logs now happen after the bus is unlocked).

* fix(multiagent): M9 - extract dropLogEveryN as a named constant

The previous 100 was a magic number inlined in the
`n%100 != 0` check. Extracted as a package-level const
`dropLogEveryN = 100` with a doc comment explaining the
rationale (balancing observability against log volume under
sustained pressure).

* test(multiagent): M10 - rename test + add real waiter-assertion test

- TestWaitForLock_OwnerMismatchOnRelease renamed to
  TestReleaseLock_NonOwnerReturnsError; the original name claimed
  it was about waiters not being woken, but the body only checked
  the error return.
- New TestWaitForLock_ReleaseByNonOwnerDoesNotWakeWaiter registers
  a real waiter, attempts a ReleaseLock from a non-owner, and
  asserts the waiter times out (rather than being woken by the
  failed ReleaseLock). This is the test the original name
  promised but never delivered.

* test(multiagent): M11 - join the WaitForResponse goroutine via a done channel

The previous TestStats_NotAffectedByWaiters launched a goroutine
that called WaitForResponse and never joined it; the goroutine
ran for 100ms after the test exited. Fixed by:
- Adding a done channel that the goroutine closes on return.
- Adding a 1-second join timeout via select on the done channel.
- The test now fails loudly if the goroutine does not return
  within 1s (which would indicate a future regression).

* fix(engine): M2 - make AddUser/AddAssistant/ForkConversation/etc nil-safe

The M2 refactor changed these methods to call
s.Persistence().DAG() and s.MemorySvc().Memory() directly. That
panics on &engine.Session{} struct literals (used by cmd tests)
where s.persist and s.memory are nil.

This commit wraps each call site in a nil-check on the parent
service (s.Persistence() / s.MemorySvc()) so that struct-literal
sessions still work the same way as before — the methods
become no-ops when their backing sub-service is nil, which
matches the previous behavior on s.ConvoDAG == nil and
s.Memory == nil.
Patel230 added a commit that referenced this pull request Aug 15, 2026
- external/yaad -> fbc0466 (PRs #53-58: incremental HNSW, graph
  restore, backup rotation, backup scheduler, refcounted process lock)
- go.mod pseudo-version bumped to match the gitlink (parity guard)
- YaadBridge starts the hourly yaad backup scheduler once per process
  on the first ready bridge (~/.yaad/data/backups, keep 7, 30d)
Patel230 added a commit that referenced this pull request Aug 15, 2026
- external/yaad -> fbc0466 (PRs #53-58: incremental HNSW, graph
  restore, backup rotation, backup scheduler, refcounted process lock)
- go.mod pseudo-version bumped to match the gitlink (parity guard)
- YaadBridge starts the hourly yaad backup scheduler once per process
  on the first ready bridge (~/.yaad/data/backups, keep 7, 30d)
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