@openscout/protocol 0.2.96 → 0.2.97

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.
package/dist/index.d.ts CHANGED
@@ -35,6 +35,8 @@ export * from "./scout-dispatch.js";
35
35
  export * from "./runtime-profiles.js";
36
36
  export * from "./runtime-execution.js";
37
37
  export * from "./runtime-catalog.js";
38
+ export * from "./scout-baseline-runtime.js";
39
+ export * from "./session-title.js";
38
40
  export * from "./reserved-vocabulary.js";
39
41
  export * from "./route-aliases.js";
40
42
  export * from "./scout-delivery.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,UAAU,CAAC;AACzB,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,UAAU,CAAC;AACzB,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -35,6 +35,8 @@ export * from "./scout-dispatch.js";
35
35
  export * from "./runtime-profiles.js";
36
36
  export * from "./runtime-execution.js";
37
37
  export * from "./runtime-catalog.js";
38
+ export * from "./scout-baseline-runtime.js";
39
+ export * from "./session-title.js";
38
40
  export * from "./reserved-vocabulary.js";
39
41
  export * from "./route-aliases.js";
40
42
  export * from "./scout-delivery.js";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Scout's cheap/fast utility runtime — not the operator's session model.
3
+ *
4
+ * Use this for Scout-owned jobs such as session titling and classification.
5
+ * Prefer the first candidate whose harness is actually available on the node:
6
+ * Codex Luna when a ChatGPT/Codex subscription is present, then an OpenRouter-
7
+ * style flash model, then other inexpensive family picks.
8
+ */
9
+ export declare const SCOUT_BASELINE_PURPOSE: "scout-utility";
10
+ export type ScoutBaselineRuntime = {
11
+ harness: string;
12
+ model: string;
13
+ effort: string | null;
14
+ label: string;
15
+ purpose: typeof SCOUT_BASELINE_PURPOSE;
16
+ };
17
+ type BaselineCandidate = {
18
+ harness: string;
19
+ aliases?: readonly string[];
20
+ model: string;
21
+ effort: string | null;
22
+ label: string;
23
+ };
24
+ /**
25
+ * Ordered cheapest-useful defaults. First matching available harness wins.
26
+ * Keep these aligned with `packages/agent-sessions` inexpensive family picks
27
+ * when those ids are launchable through a Scout harness.
28
+ */
29
+ export declare const SCOUT_BASELINE_CANDIDATES: readonly BaselineCandidate[];
30
+ /** First cheap/fast Scout utility runtime the node can actually launch. */
31
+ export declare function resolveScoutBaselineRuntime(availableHarnesses: readonly string[]): ScoutBaselineRuntime | null;
32
+ export {};
33
+ //# sourceMappingURL=scout-baseline-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scout-baseline-runtime.d.ts","sourceRoot":"","sources":["../src/scout-baseline-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,eAAO,MAAM,sBAAsB,EAAG,eAAwB,CAAC;AAE/D,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,sBAAsB,CAAC;CACxC,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,SAAS,iBAAiB,EAiCjE,CAAC;AAWF,2EAA2E;AAC3E,wBAAgB,2BAA2B,CACzC,kBAAkB,EAAE,SAAS,MAAM,EAAE,GACpC,oBAAoB,GAAG,IAAI,CAkB7B"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Scout's cheap/fast utility runtime — not the operator's session model.
3
+ *
4
+ * Use this for Scout-owned jobs such as session titling and classification.
5
+ * Prefer the first candidate whose harness is actually available on the node:
6
+ * Codex Luna when a ChatGPT/Codex subscription is present, then an OpenRouter-
7
+ * style flash model, then other inexpensive family picks.
8
+ */
9
+ export const SCOUT_BASELINE_PURPOSE = "scout-utility";
10
+ /**
11
+ * Ordered cheapest-useful defaults. First matching available harness wins.
12
+ * Keep these aligned with `packages/agent-sessions` inexpensive family picks
13
+ * when those ids are launchable through a Scout harness.
14
+ */
15
+ export const SCOUT_BASELINE_CANDIDATES = [
16
+ {
17
+ harness: "codex",
18
+ model: "gpt-5.6-luna",
19
+ effort: "low",
20
+ label: "Luna",
21
+ },
22
+ {
23
+ harness: "opencode",
24
+ model: "deepseek-v4-flash",
25
+ effort: null,
26
+ label: "DeepSeek Flash",
27
+ },
28
+ {
29
+ harness: "grok",
30
+ aliases: ["grok-acp"],
31
+ model: "grok-4.20-0309-non-reasoning",
32
+ effort: null,
33
+ label: "Grok Fast",
34
+ },
35
+ {
36
+ harness: "claude",
37
+ model: "claude-haiku-4-5",
38
+ effort: "low",
39
+ label: "Haiku",
40
+ },
41
+ {
42
+ harness: "kimi",
43
+ aliases: ["kimi-acp"],
44
+ model: "kimi-k2.5",
45
+ effort: null,
46
+ label: "Kimi",
47
+ },
48
+ ];
49
+ function normalizeHarness(value) {
50
+ return value.trim().toLowerCase();
51
+ }
52
+ function candidateMatches(candidate, harness) {
53
+ if (candidate.harness === harness)
54
+ return true;
55
+ return candidate.aliases?.some((alias) => alias === harness) === true;
56
+ }
57
+ /** First cheap/fast Scout utility runtime the node can actually launch. */
58
+ export function resolveScoutBaselineRuntime(availableHarnesses) {
59
+ const available = new Set(availableHarnesses.map(normalizeHarness).filter(Boolean));
60
+ if (available.size === 0)
61
+ return null;
62
+ for (const candidate of SCOUT_BASELINE_CANDIDATES) {
63
+ const harness = [...available].find((id) => candidateMatches(candidate, id));
64
+ if (!harness)
65
+ continue;
66
+ return {
67
+ harness: candidate.harness,
68
+ model: candidate.model,
69
+ effort: candidate.effort,
70
+ label: candidate.label,
71
+ purpose: SCOUT_BASELINE_PURPOSE,
72
+ };
73
+ }
74
+ return null;
75
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Logical session titles for Scout lists.
3
+ *
4
+ * Harness first-turns are often XML wrappers (`<local-command-caveat>`,
5
+ * `<task-notification>`) or Codex/Kimi/Grok generated names. Prefer a
6
+ * harness-provided title when the caller has one; otherwise strip wrappers
7
+ * and keep the first meaningful user line. LLM titling, when added, should
8
+ * call `resolveScoutBaselineRuntime` rather than the operator's session model.
9
+ */
10
+ /** True when a stored title is a harness wrapper dump, not a session name. */
11
+ export declare function isHarnessWrapperTitle(value: string | null | undefined): boolean;
12
+ /**
13
+ * Turn a first user turn or harness name into a list-safe title.
14
+ * Returns null when nothing human remains — callers should use a project
15
+ * or harness fallback.
16
+ */
17
+ export declare function logicalSessionTitle(source: string | null | undefined, fallback?: string | null): string | null;
18
+ //# sourceMappingURL=session-title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-title.d.ts","sourceRoot":"","sources":["../src/session-title.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA4CH,8EAA8E;AAC9E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAI/E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GACvB,MAAM,GAAG,IAAI,CAaf"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Logical session titles for Scout lists.
3
+ *
4
+ * Harness first-turns are often XML wrappers (`<local-command-caveat>`,
5
+ * `<task-notification>`) or Codex/Kimi/Grok generated names. Prefer a
6
+ * harness-provided title when the caller has one; otherwise strip wrappers
7
+ * and keep the first meaningful user line. LLM titling, when added, should
8
+ * call `resolveScoutBaselineRuntime` rather than the operator's session model.
9
+ */
10
+ const TITLE_MAX_CHARS = 72;
11
+ const WRAPPER_TAGS = [
12
+ "local-command-caveat",
13
+ "task-notification",
14
+ "command-name",
15
+ "command-message",
16
+ "command-args",
17
+ "local-command",
18
+ ];
19
+ const BOILERPLATE = [
20
+ /^caveat:\s*the messages below were generated by the user/iu,
21
+ /^do not respond to these messages/iu,
22
+ /^\[user\]$/iu,
23
+ ];
24
+ function compact(value) {
25
+ return value.replace(/\s+/gu, " ").trim();
26
+ }
27
+ function stripWrapperTags(value) {
28
+ let next = value;
29
+ for (const tag of WRAPPER_TAGS) {
30
+ const block = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "giu");
31
+ const empty = new RegExp(`<${tag}\\b[^>]*\\/>`, "giu");
32
+ next = next.replace(block, "\n").replace(empty, "\n");
33
+ }
34
+ next = next.replace(/<\/?[A-Za-z][\w:-]*\b[^>]*>/gu, "\n");
35
+ return next;
36
+ }
37
+ function isBoilerplate(value) {
38
+ return BOILERPLATE.some((pattern) => pattern.test(value));
39
+ }
40
+ function looksLikeWrapperDump(value) {
41
+ const lowered = value.toLowerCase();
42
+ return WRAPPER_TAGS.some((tag) => lowered.includes(`<${tag}`))
43
+ || lowered.includes("caveat: the messages below were generated");
44
+ }
45
+ /** True when a stored title is a harness wrapper dump, not a session name. */
46
+ export function isHarnessWrapperTitle(value) {
47
+ const trimmed = value?.trim() ?? "";
48
+ if (!trimmed)
49
+ return true;
50
+ return looksLikeWrapperDump(trimmed) || isBoilerplate(trimmed);
51
+ }
52
+ /**
53
+ * Turn a first user turn or harness name into a list-safe title.
54
+ * Returns null when nothing human remains — callers should use a project
55
+ * or harness fallback.
56
+ */
57
+ export function logicalSessionTitle(source, fallback) {
58
+ const raw = source?.trim() ?? "";
59
+ const lines = (raw ? stripWrapperTags(raw) : "")
60
+ .split(/\n+/u)
61
+ .map((line) => compact(line))
62
+ .filter((line) => line.length > 0 && !isBoilerplate(line) && !looksLikeWrapperDump(line));
63
+ const candidate = lines.find((line) => line.length >= 12) ?? lines[0] ?? "";
64
+ if (!candidate) {
65
+ const fallbackText = fallback?.trim() ?? "";
66
+ return fallbackText ? compact(fallbackText).slice(0, TITLE_MAX_CHARS) : null;
67
+ }
68
+ if (candidate.length <= TITLE_MAX_CHARS)
69
+ return candidate;
70
+ return `${candidate.slice(0, TITLE_MAX_CHARS - 1).trimEnd()}…`;
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openscout/protocol",
3
- "version": "0.2.96",
3
+ "version": "0.2.97",
4
4
  "description": "Typed protocol for the OpenScout local control plane",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -57,5 +57,5 @@
57
57
  "devDependencies": {
58
58
  "typescript": "^5"
59
59
  },
60
- "gitHead": "32b205d7fbc6d0be7768f208eabcecbd7fa371b6"
60
+ "gitHead": "aa8585053c67291fbdf44d664f7f00ef4f5bf637"
61
61
  }
package/src/index.ts CHANGED
@@ -35,6 +35,8 @@ export * from "./scout-dispatch.js";
35
35
  export * from "./runtime-profiles.js";
36
36
  export * from "./runtime-execution.js";
37
37
  export * from "./runtime-catalog.js";
38
+ export * from "./scout-baseline-runtime.js";
39
+ export * from "./session-title.js";
38
40
  export * from "./reserved-vocabulary.js";
39
41
  export * from "./route-aliases.js";
40
42
  export * from "./scout-delivery.js";
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Scout's cheap/fast utility runtime — not the operator's session model.
3
+ *
4
+ * Use this for Scout-owned jobs such as session titling and classification.
5
+ * Prefer the first candidate whose harness is actually available on the node:
6
+ * Codex Luna when a ChatGPT/Codex subscription is present, then an OpenRouter-
7
+ * style flash model, then other inexpensive family picks.
8
+ */
9
+
10
+ export const SCOUT_BASELINE_PURPOSE = "scout-utility" as const;
11
+
12
+ export type ScoutBaselineRuntime = {
13
+ harness: string;
14
+ model: string;
15
+ effort: string | null;
16
+ label: string;
17
+ purpose: typeof SCOUT_BASELINE_PURPOSE;
18
+ };
19
+
20
+ type BaselineCandidate = {
21
+ harness: string;
22
+ aliases?: readonly string[];
23
+ model: string;
24
+ effort: string | null;
25
+ label: string;
26
+ };
27
+
28
+ /**
29
+ * Ordered cheapest-useful defaults. First matching available harness wins.
30
+ * Keep these aligned with `packages/agent-sessions` inexpensive family picks
31
+ * when those ids are launchable through a Scout harness.
32
+ */
33
+ export const SCOUT_BASELINE_CANDIDATES: readonly BaselineCandidate[] = [
34
+ {
35
+ harness: "codex",
36
+ model: "gpt-5.6-luna",
37
+ effort: "low",
38
+ label: "Luna",
39
+ },
40
+ {
41
+ harness: "opencode",
42
+ model: "deepseek-v4-flash",
43
+ effort: null,
44
+ label: "DeepSeek Flash",
45
+ },
46
+ {
47
+ harness: "grok",
48
+ aliases: ["grok-acp"],
49
+ model: "grok-4.20-0309-non-reasoning",
50
+ effort: null,
51
+ label: "Grok Fast",
52
+ },
53
+ {
54
+ harness: "claude",
55
+ model: "claude-haiku-4-5",
56
+ effort: "low",
57
+ label: "Haiku",
58
+ },
59
+ {
60
+ harness: "kimi",
61
+ aliases: ["kimi-acp"],
62
+ model: "kimi-k2.5",
63
+ effort: null,
64
+ label: "Kimi",
65
+ },
66
+ ];
67
+
68
+ function normalizeHarness(value: string): string {
69
+ return value.trim().toLowerCase();
70
+ }
71
+
72
+ function candidateMatches(candidate: BaselineCandidate, harness: string): boolean {
73
+ if (candidate.harness === harness) return true;
74
+ return candidate.aliases?.some((alias) => alias === harness) === true;
75
+ }
76
+
77
+ /** First cheap/fast Scout utility runtime the node can actually launch. */
78
+ export function resolveScoutBaselineRuntime(
79
+ availableHarnesses: readonly string[],
80
+ ): ScoutBaselineRuntime | null {
81
+ const available = new Set(
82
+ availableHarnesses.map(normalizeHarness).filter(Boolean),
83
+ );
84
+ if (available.size === 0) return null;
85
+
86
+ for (const candidate of SCOUT_BASELINE_CANDIDATES) {
87
+ const harness = [...available].find((id) => candidateMatches(candidate, id));
88
+ if (!harness) continue;
89
+ return {
90
+ harness: candidate.harness,
91
+ model: candidate.model,
92
+ effort: candidate.effort,
93
+ label: candidate.label,
94
+ purpose: SCOUT_BASELINE_PURPOSE,
95
+ };
96
+ }
97
+ return null;
98
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Logical session titles for Scout lists.
3
+ *
4
+ * Harness first-turns are often XML wrappers (`<local-command-caveat>`,
5
+ * `<task-notification>`) or Codex/Kimi/Grok generated names. Prefer a
6
+ * harness-provided title when the caller has one; otherwise strip wrappers
7
+ * and keep the first meaningful user line. LLM titling, when added, should
8
+ * call `resolveScoutBaselineRuntime` rather than the operator's session model.
9
+ */
10
+
11
+ const TITLE_MAX_CHARS = 72;
12
+
13
+ const WRAPPER_TAGS = [
14
+ "local-command-caveat",
15
+ "task-notification",
16
+ "command-name",
17
+ "command-message",
18
+ "command-args",
19
+ "local-command",
20
+ ];
21
+
22
+ const BOILERPLATE = [
23
+ /^caveat:\s*the messages below were generated by the user/iu,
24
+ /^do not respond to these messages/iu,
25
+ /^\[user\]$/iu,
26
+ ];
27
+
28
+ function compact(value: string): string {
29
+ return value.replace(/\s+/gu, " ").trim();
30
+ }
31
+
32
+ function stripWrapperTags(value: string): string {
33
+ let next = value;
34
+ for (const tag of WRAPPER_TAGS) {
35
+ const block = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "giu");
36
+ const empty = new RegExp(`<${tag}\\b[^>]*\\/>`, "giu");
37
+ next = next.replace(block, "\n").replace(empty, "\n");
38
+ }
39
+ next = next.replace(/<\/?[A-Za-z][\w:-]*\b[^>]*>/gu, "\n");
40
+ return next;
41
+ }
42
+
43
+ function isBoilerplate(value: string): boolean {
44
+ return BOILERPLATE.some((pattern) => pattern.test(value));
45
+ }
46
+
47
+ function looksLikeWrapperDump(value: string): boolean {
48
+ const lowered = value.toLowerCase();
49
+ return WRAPPER_TAGS.some((tag) => lowered.includes(`<${tag}`))
50
+ || lowered.includes("caveat: the messages below were generated");
51
+ }
52
+
53
+ /** True when a stored title is a harness wrapper dump, not a session name. */
54
+ export function isHarnessWrapperTitle(value: string | null | undefined): boolean {
55
+ const trimmed = value?.trim() ?? "";
56
+ if (!trimmed) return true;
57
+ return looksLikeWrapperDump(trimmed) || isBoilerplate(trimmed);
58
+ }
59
+
60
+ /**
61
+ * Turn a first user turn or harness name into a list-safe title.
62
+ * Returns null when nothing human remains — callers should use a project
63
+ * or harness fallback.
64
+ */
65
+ export function logicalSessionTitle(
66
+ source: string | null | undefined,
67
+ fallback?: string | null,
68
+ ): string | null {
69
+ const raw = source?.trim() ?? "";
70
+ const lines = (raw ? stripWrapperTags(raw) : "")
71
+ .split(/\n+/u)
72
+ .map((line) => compact(line))
73
+ .filter((line) => line.length > 0 && !isBoilerplate(line) && !looksLikeWrapperDump(line));
74
+ const candidate = lines.find((line) => line.length >= 12) ?? lines[0] ?? "";
75
+ if (!candidate) {
76
+ const fallbackText = fallback?.trim() ?? "";
77
+ return fallbackText ? compact(fallbackText).slice(0, TITLE_MAX_CHARS) : null;
78
+ }
79
+ if (candidate.length <= TITLE_MAX_CHARS) return candidate;
80
+ return `${candidate.slice(0, TITLE_MAX_CHARS - 1).trimEnd()}…`;
81
+ }