harnery 0.31.5 → 0.31.6

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.
@@ -162,7 +162,10 @@ export declare function resolveOwnerByPidmap(root: string): {
162
162
  * `instance_id`, or null if there's no session-id env var or no live heartbeat
163
163
  * carries it. "Live" reuses the same 10-minute freshness window the singleton
164
164
  * fallback applies, so a stale heartbeat from a prior session of the same id
165
- * doesn't resolve.
165
+ * doesn't resolve. When legacy Cursor Glass state contains both the canonical
166
+ * bare id and its raw `bc-` alias, candidate order wins first; ties prefer a
167
+ * named heartbeat, then the newest heartbeat, with instance id as a stable
168
+ * final tie-breaker. Filesystem enumeration order never decides identity.
166
169
  *
167
170
  * Exported for unit testing with an injectable root.
168
171
  */
@@ -1 +1 @@
1
- {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"coord-client.d.ts","sourceRoot":"","sources":["../../../src/core/agents/coord-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAWhE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAI5E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOhF;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yIAAyI;IACzI,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA+B7E;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7D;AAeD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGtD;AAiED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,mFAAmF;AACnF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2CxD;AAgCD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,GAAG,MAAM,CAAC;CAC5F,CAwCA;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG;IAClD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,QAAQ,GAAG,iBAAiB,GAAG,MAAM,CAAC;CAC/C,CAyBA;AA0DD;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgDpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAMlE;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAIxC"}
@@ -482,7 +482,10 @@ function shouldPreferSessionEnv() {
482
482
  * `instance_id`, or null if there's no session-id env var or no live heartbeat
483
483
  * carries it. "Live" reuses the same 10-minute freshness window the singleton
484
484
  * fallback applies, so a stale heartbeat from a prior session of the same id
485
- * doesn't resolve.
485
+ * doesn't resolve. When legacy Cursor Glass state contains both the canonical
486
+ * bare id and its raw `bc-` alias, candidate order wins first; ties prefer a
487
+ * named heartbeat, then the newest heartbeat, with instance id as a stable
488
+ * final tie-breaker. Filesystem enumeration order never decides identity.
486
489
  *
487
490
  * Exported for unit testing with an injectable root.
488
491
  */
@@ -502,24 +505,38 @@ export function resolveOwnerBySessionEnv(root) {
502
505
  catch {
503
506
  return null;
504
507
  }
508
+ const matches = [];
505
509
  for (const file of files) {
506
510
  if (!file.endsWith(".json"))
507
511
  continue;
508
512
  try {
509
513
  const parsed = JSON.parse(readFileSync(resolve(activeDir, file), "utf8"));
510
- if (!parsed || !sessionIds.includes(parsed.session_id))
514
+ if (!parsed)
515
+ continue;
516
+ const sessionPreference = sessionIds.indexOf(parsed.session_id);
517
+ if (sessionPreference === -1)
511
518
  continue;
512
519
  if (typeof parsed.instance_id !== "string")
513
520
  continue;
514
521
  const ts = Date.parse(parsed.last_heartbeat);
515
- if (Number.isFinite(ts) && ts >= cutoffMs)
516
- return parsed.instance_id;
522
+ if (!Number.isFinite(ts) || ts < cutoffMs)
523
+ continue;
524
+ matches.push({
525
+ instanceId: parsed.instance_id,
526
+ sessionPreference,
527
+ hasName: typeof parsed.name === "string" && parsed.name.trim().length > 0,
528
+ lastHeartbeatMs: ts,
529
+ });
517
530
  }
518
531
  catch {
519
532
  // skip malformed
520
533
  }
521
534
  }
522
- return null;
535
+ matches.sort((a, b) => a.sessionPreference - b.sessionPreference ||
536
+ Number(b.hasName) - Number(a.hasName) ||
537
+ b.lastHeartbeatMs - a.lastHeartbeatMs ||
538
+ a.instanceId.localeCompare(b.instanceId));
539
+ return matches[0]?.instanceId ?? null;
523
540
  }
524
541
  /**
525
542
  * Return the instance_id of the sole live agent in this coord root, or null
@@ -40,7 +40,7 @@ export interface ParsedPayload {
40
40
  * Parse the raw stdin payload string for any adapter. Returns null when JSON
41
41
  * parse fails (Cursor occasionally fires hooks with no payload).
42
42
  */
43
- export declare function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | null;
43
+ export declare function parsePayload(raw: string, adapter: Adapter): ParsedPayload | null;
44
44
  /**
45
45
  * Pull the bash command string out of a Bash/Shell tool_input. Returns
46
46
  * undefined for non-shell tools.
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAkCjF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,SAAS,EAAE,OAAO,GACjB,MAAM,GAAG,SAAS,CASpB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAO7E;AAmBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAChB;IAAE,UAAU,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAiCjE;AAED,MAAM,MAAM,mBAAmB,GAC3B,eAAe,GACf,aAAa,GACb,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,cAAc,GACd,eAAe,GACf,uBAAuB,GACvB,4BAA4B,GAC5B,8BAA8B,CAAC"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/adapter/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CA+ChF;AAQD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,SAAS,EAAE,OAAO,GACjB,MAAM,GAAG,SAAS,CASpB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAO7E;AAmBD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAChB;IAAE,UAAU,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAiCjE;AAED,MAAM,MAAM,mBAAmB,GAC3B,eAAe,GACf,aAAa,GACb,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,cAAc,GACd,eAAe,GACf,uBAAuB,GACvB,4BAA4B,GAC5B,8BAA8B,CAAC"}
@@ -13,7 +13,7 @@
13
13
  * Parse the raw stdin payload string for any adapter. Returns null when JSON
14
14
  * parse fails (Cursor occasionally fires hooks with no payload).
15
15
  */
16
- export function parsePayload(raw, _adapter) {
16
+ export function parsePayload(raw, adapter) {
17
17
  if (!raw || raw.trim().length === 0)
18
18
  return null;
19
19
  let json;
@@ -23,13 +23,24 @@ export function parsePayload(raw, _adapter) {
23
23
  catch {
24
24
  return null;
25
25
  }
26
+ const sessionId = normalizeSessionId(adapter, pickStr(json, "session_id"));
27
+ const conversationId = normalizeSessionId(adapter, pickStr(json, "conversation_id"));
28
+ const parentSessionId = normalizeSessionId(adapter, pickStr(json, "parent_session_id"));
29
+ const normalizedRaw = adapter === "cursor"
30
+ ? {
31
+ ...json,
32
+ ...(sessionId ? { session_id: sessionId } : {}),
33
+ ...(conversationId ? { conversation_id: conversationId } : {}),
34
+ ...(parentSessionId ? { parent_session_id: parentSessionId } : {}),
35
+ }
36
+ : json;
26
37
  return {
27
38
  hook_event_name: pickStr(json, "hook_event_name"),
28
- session_id: pickStr(json, "session_id"),
39
+ session_id: sessionId,
29
40
  agent_id: pickStr(json, "agent_id"),
30
41
  subagent_id: pickStr(json, "subagent_id"),
31
- conversation_id: pickStr(json, "conversation_id"),
32
- parent_session_id: pickStr(json, "parent_session_id"),
42
+ conversation_id: conversationId,
43
+ parent_session_id: parentSessionId,
33
44
  turn_id: pickStr(json, "turn_id"),
34
45
  parent_turn_id: pickStr(json, "parent_turn_id"),
35
46
  transcript_path: pickStr(json, "transcript_path"),
@@ -46,9 +57,15 @@ export function parsePayload(raw, _adapter) {
46
57
  clean_exit: pickBool(json, "clean_exit"),
47
58
  exit_status: pickStr(json, "exit_status"),
48
59
  reason: pickStr(json, "reason"),
49
- raw: json,
60
+ raw: normalizedRaw,
50
61
  };
51
62
  }
63
+ /** Cursor Glass prefixes conversation ids with `bc-`; coordination uses the bare id everywhere. */
64
+ function normalizeSessionId(adapter, value) {
65
+ if (adapter !== "cursor" || !value?.startsWith("bc-") || value.length <= 3)
66
+ return value;
67
+ return value.slice(3);
68
+ }
52
69
  /**
53
70
  * Pull the bash command string out of a Bash/Shell tool_input. Returns
54
71
  * undefined for non-shell tools.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.31.5",
3
+ "version": "0.31.6",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -528,7 +528,10 @@ function shouldPreferSessionEnv(): boolean {
528
528
  * `instance_id`, or null if there's no session-id env var or no live heartbeat
529
529
  * carries it. "Live" reuses the same 10-minute freshness window the singleton
530
530
  * fallback applies, so a stale heartbeat from a prior session of the same id
531
- * doesn't resolve.
531
+ * doesn't resolve. When legacy Cursor Glass state contains both the canonical
532
+ * bare id and its raw `bc-` alias, candidate order wins first; ties prefer a
533
+ * named heartbeat, then the newest heartbeat, with instance id as a stable
534
+ * final tie-breaker. Filesystem enumeration order never decides identity.
532
535
  *
533
536
  * Exported for unit testing with an injectable root.
534
537
  */
@@ -546,19 +549,40 @@ export function resolveOwnerBySessionEnv(root: string): string | null {
546
549
  } catch {
547
550
  return null;
548
551
  }
552
+ const matches: Array<{
553
+ instanceId: string;
554
+ sessionPreference: number;
555
+ hasName: boolean;
556
+ lastHeartbeatMs: number;
557
+ }> = [];
549
558
  for (const file of files) {
550
559
  if (!file.endsWith(".json")) continue;
551
560
  try {
552
561
  const parsed = JSON.parse(readFileSync(resolve(activeDir, file), "utf8"));
553
- if (!parsed || !sessionIds.includes(parsed.session_id)) continue;
562
+ if (!parsed) continue;
563
+ const sessionPreference = sessionIds.indexOf(parsed.session_id);
564
+ if (sessionPreference === -1) continue;
554
565
  if (typeof parsed.instance_id !== "string") continue;
555
566
  const ts = Date.parse(parsed.last_heartbeat);
556
- if (Number.isFinite(ts) && ts >= cutoffMs) return parsed.instance_id;
567
+ if (!Number.isFinite(ts) || ts < cutoffMs) continue;
568
+ matches.push({
569
+ instanceId: parsed.instance_id,
570
+ sessionPreference,
571
+ hasName: typeof parsed.name === "string" && parsed.name.trim().length > 0,
572
+ lastHeartbeatMs: ts,
573
+ });
557
574
  } catch {
558
575
  // skip malformed
559
576
  }
560
577
  }
561
- return null;
578
+ matches.sort(
579
+ (a, b) =>
580
+ a.sessionPreference - b.sessionPreference ||
581
+ Number(b.hasName) - Number(a.hasName) ||
582
+ b.lastHeartbeatMs - a.lastHeartbeatMs ||
583
+ a.instanceId.localeCompare(b.instanceId),
584
+ );
585
+ return matches[0]?.instanceId ?? null;
562
586
  }
563
587
 
564
588
  /**
@@ -43,7 +43,7 @@ export interface ParsedPayload {
43
43
  * Parse the raw stdin payload string for any adapter. Returns null when JSON
44
44
  * parse fails (Cursor occasionally fires hooks with no payload).
45
45
  */
46
- export function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | null {
46
+ export function parsePayload(raw: string, adapter: Adapter): ParsedPayload | null {
47
47
  if (!raw || raw.trim().length === 0) return null;
48
48
  let json: Record<string, unknown>;
49
49
  try {
@@ -52,13 +52,26 @@ export function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | nu
52
52
  return null;
53
53
  }
54
54
 
55
+ const sessionId = normalizeSessionId(adapter, pickStr(json, "session_id"));
56
+ const conversationId = normalizeSessionId(adapter, pickStr(json, "conversation_id"));
57
+ const parentSessionId = normalizeSessionId(adapter, pickStr(json, "parent_session_id"));
58
+ const normalizedRaw =
59
+ adapter === "cursor"
60
+ ? {
61
+ ...json,
62
+ ...(sessionId ? { session_id: sessionId } : {}),
63
+ ...(conversationId ? { conversation_id: conversationId } : {}),
64
+ ...(parentSessionId ? { parent_session_id: parentSessionId } : {}),
65
+ }
66
+ : json;
67
+
55
68
  return {
56
69
  hook_event_name: pickStr(json, "hook_event_name"),
57
- session_id: pickStr(json, "session_id"),
70
+ session_id: sessionId,
58
71
  agent_id: pickStr(json, "agent_id"),
59
72
  subagent_id: pickStr(json, "subagent_id"),
60
- conversation_id: pickStr(json, "conversation_id"),
61
- parent_session_id: pickStr(json, "parent_session_id"),
73
+ conversation_id: conversationId,
74
+ parent_session_id: parentSessionId,
62
75
  turn_id: pickStr(json, "turn_id"),
63
76
  parent_turn_id: pickStr(json, "parent_turn_id"),
64
77
  transcript_path: pickStr(json, "transcript_path"),
@@ -75,10 +88,16 @@ export function parsePayload(raw: string, _adapter: Adapter): ParsedPayload | nu
75
88
  clean_exit: pickBool(json, "clean_exit"),
76
89
  exit_status: pickStr(json, "exit_status"),
77
90
  reason: pickStr(json, "reason"),
78
- raw: json,
91
+ raw: normalizedRaw,
79
92
  };
80
93
  }
81
94
 
95
+ /** Cursor Glass prefixes conversation ids with `bc-`; coordination uses the bare id everywhere. */
96
+ function normalizeSessionId(adapter: Adapter, value: string | undefined): string | undefined {
97
+ if (adapter !== "cursor" || !value?.startsWith("bc-") || value.length <= 3) return value;
98
+ return value.slice(3);
99
+ }
100
+
82
101
  /**
83
102
  * Pull the bash command string out of a Bash/Shell tool_input. Returns
84
103
  * undefined for non-shell tools.