talking-stick 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,7 +31,11 @@ export function handleJoinCommand(runtime, parsed) {
31
31
  });
32
32
  upsertSessionFromJoin(identity, joined);
33
33
  printResult(parsed, joined, () => {
34
- return `Joined ${joined.canonical_path} as ${joined.agent_id}`;
34
+ const lines = [`Joined ${joined.canonical_path} as ${joined.agent_id}`];
35
+ if (joined.warning) {
36
+ lines.push(`Warning: ${joined.warning}`);
37
+ }
38
+ return lines.join("\n");
35
39
  });
36
40
  }
37
41
  export function handleLeaveCommand(runtime, parsed) {
package/dist/service.js CHANGED
@@ -682,7 +682,11 @@ export class TalkingStickService {
682
682
  if (forceNew) {
683
683
  const exactRoom = this.findRoomByCanonicalPath(resolved.canonical_context_path);
684
684
  if (exactRoom) {
685
- return { room: exactRoom, joinedExistingRoom: true };
685
+ return {
686
+ room: exactRoom,
687
+ joinedExistingRoom: true,
688
+ warning: `force_new had no effect: a room already exists at ${exactRoom.canonical_path}. force_new only creates a nested room when an ancestor room exists; same-path duplicates are not supported. To get a fresh room for a separate topic, join a distinct subpath.`
689
+ };
686
690
  }
687
691
  return {
688
692
  room: this.createRoom(resolved.canonical_context_path, timestamp),
@@ -0,0 +1,68 @@
1
+ # Talking Stick 0.1.4
2
+
3
+ Date: 2026-04-30
4
+
5
+ Patch release that clarifies the existing `force_new` semantics on `join_path`,
6
+ makes the resulting warnings visible to human CLI users, and stabilizes the
7
+ test suite.
8
+
9
+ ## Fixed
10
+
11
+ ### `force_new` no-op on exact-path joins now surfaces a warning
12
+
13
+ `join_path` has always had two shapes for `force_new=true`: it creates a nested
14
+ room when a different ancestor room exists, and it joins the existing room
15
+ when one already exists at the exact `canonical_path` (because
16
+ `path_rooms.canonical_path` is `UNIQUE`). The second shape is a deliberate
17
+ no-op — `force_new` cannot duplicate a room at the same canonical path — but
18
+ prior versions returned the existing room silently with `joined_existing_room:
19
+ true` and no further signal. Operators and harnesses who flipped `--force-new`
20
+ expecting a fresh room had no way to tell their request had no effect.
21
+
22
+ `join_path` now returns a `warning` field on this no-op path explaining what
23
+ happened and pointing the caller at the only remedy: join a distinct subpath
24
+ (for example a topic-scoped subdirectory) to get a fresh room. The nested-room
25
+ warning that has always existed is unchanged. Both warnings are now also
26
+ rendered in the default `tt join` text output, not just `--json`.
27
+
28
+ In practice this means a Claude Code or Codex harness asking for a new "topic
29
+ room" at the same path now sees an explicit signal that the API only knows
30
+ about path-scoped rooms, instead of silently working off the existing room.
31
+
32
+ The bundled skill's *While waiting* section also gains a more proactive
33
+ framing: the wait window is the right place to re-read the holder's last
34
+ handoff, follow its `artifacts[]`, investigate, and surface findings via
35
+ `add_note` — not idle sleep.
36
+
37
+ ### Contention test no longer races the room-purge clock
38
+
39
+ `tests/talking-stick.test.ts > only one process can claim an idle room under
40
+ contention` reproduced as a `room_not_found` failure when the wall-clock date
41
+ crossed the `idleRoomTtlMs` window relative to the parent test's fake clock
42
+ fixed at 2026-04-22. The contention worker spawned in
43
+ `tests/fixtures/claim-worker.ts` constructed its `TalkingStickService` with
44
+ the real `Date.now()`, so the worker's `purgeExpiredIdleRooms` call evicted
45
+ the room that the parent had just created under the fake clock. The fix
46
+ threads the parent's fake-clock ISO timestamp into the worker so the service
47
+ shares the same `now()` source, and the test passes deterministically
48
+ regardless of wall-clock date.
49
+
50
+ ### Identity-resolver memoization test no longer asserts internal call counts
51
+
52
+ `tests/mcp-server.test.ts > createConnectionIdentityResolver > memoizes
53
+ derived identity per session and re-derives on override` was written when
54
+ `deriveMcpHarnessIdentity` made one `inspector.inspect` call per derive.
55
+ Since 0.1.3's ancestry-walk fix (commit `ab4e843`), a single derive may walk
56
+ multiple parent processes. The resolver-level memoization is still correct;
57
+ the test was asserting the wrong invariant. Updated to assert call-count
58
+ *deltas* across resolver invocations (no growth on memoized hits, growth on
59
+ fresh sessions and overrides) instead of absolute counts, so the test
60
+ remains robust to changes in ancestry-walk depth.
61
+
62
+ ## Verification
63
+
64
+ - `npm run typecheck`
65
+ - `npm test` — 225 tests across 14 files, all passing under Node 24.11.0
66
+ - `npm run build`
67
+ - `git diff --check`
68
+ - `npm pack --dry-run --ignore-scripts`
@@ -316,7 +316,7 @@ Resolution:
316
316
  2. Resolve the preferred workspace root.
317
317
  3. Walk up from the canonical `context_path` to the preferred workspace root looking for an existing room.
318
318
  4. If found and `force_new = false`: join the deepest existing ancestor room.
319
- 5. If found and `force_new = true`: create a nested room at the canonical `context_path`, returning a warning that an ancestor room exists. If a room already exists at that exact path, join it.
319
+ 5. If found and `force_new = true`: create a nested room at the canonical `context_path`, returning a warning that an ancestor room exists. If a room already exists at the exact `canonical_context_path`, join it and return a warning that `force_new` was a no-op. `force_new` only creates *nested* rooms; it never duplicates a room at the same canonical path because `path_rooms.canonical_path` is unique. Callers that want a fresh room for a separate topic must join a distinct subpath.
320
320
  6. If not found: create a new room at the preferred workspace root.
321
321
 
322
322
  The response includes the resolved `room_id`, the `canonical_path` the agent actually joined (which may differ from the request path when workspace root resolution or ancestor lookup redirected the call), the effective room policy (including `heartbeat_interval_ms`), and a `handoff_template` hint describing the expected handoff shape. For the MVP this template is static server-wide; room-specific prompting can be added later if real workflows need it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talking-stick",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "MCP coordination server for path-scoped agent handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -91,7 +91,7 @@ If you do not have the stick:
91
91
  - it is fine to read, plan, review, or help the user think — or any other work that does not mutate shared state
92
92
  - tell the user who currently holds or is reserved the turn when that is useful
93
93
 
94
- If you notice something the current owner should know a subtle invariant near code they are about to touch, a related bug you spotted while reading, a pointer to a doc — leave a note with `add_note` instead of sitting on it until your next turn. Notes do not grant permission to edit shared files; they are observations and pointers, not coordination bypasses.
94
+ The wait is for *active* non-mutating work, not idle sleep. Re-read the holder's last handoff, follow up on its `artifacts[]`, investigate the area they are touching, and rethink the plan from your own angle. If you find something the holder should know — a missed invariant, a related bug, a sharper plan — leave a note with `add_note` rather than sitting on it until your next turn. Notes do not grant permission to edit shared files; they are observations and pointers, not coordination bypasses. The point: while you wait you can still move the work forward by feeding the holder, not by stalling.
95
95
 
96
96
  When you do take the stick, first read the attached handoff and load any useful `artifacts[]`, then run `list_notes` once so you see what other members left for you. The owner's turn is the right place to act on a note, not to debate it with its author mid-turn.
97
97