@polderlabs/bizar-omp 0.7.6 → 0.7.8

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.
Files changed (59) hide show
  1. package/README.md +16 -10
  2. package/dist/cli/agents-ink.d.ts +34 -0
  3. package/dist/cli/agents-ink.d.ts.map +1 -1
  4. package/dist/cli/agents-ink.js +120 -30
  5. package/dist/cli/agents-ink.js.map +1 -1
  6. package/dist/cli/agents.d.ts +1 -0
  7. package/dist/cli/agents.d.ts.map +1 -1
  8. package/dist/cli/agents.js +37 -13
  9. package/dist/cli/agents.js.map +1 -1
  10. package/dist/cli/doctor.d.ts +4 -4
  11. package/dist/core/types.d.ts +13 -0
  12. package/dist/core/types.d.ts.map +1 -1
  13. package/dist/core/types.js +15 -1
  14. package/dist/core/types.js.map +1 -1
  15. package/dist/extension.d.ts +2 -0
  16. package/dist/extension.d.ts.map +1 -1
  17. package/dist/extension.js +161 -59
  18. package/dist/extension.js.map +1 -1
  19. package/dist/omp/compatibility.d.ts +5 -5
  20. package/dist/omp/compatibility.d.ts.map +1 -1
  21. package/dist/omp/compatibility.js +2 -2
  22. package/dist/omp/compatibility.js.map +1 -1
  23. package/dist/omp/daemon-service.d.ts.map +1 -1
  24. package/dist/omp/daemon-service.js +4 -1
  25. package/dist/omp/daemon-service.js.map +1 -1
  26. package/dist/omp/lifecycle.d.ts +1 -1
  27. package/dist/omp/lifecycle.d.ts.map +1 -1
  28. package/dist/omp/lifecycle.js +2 -1
  29. package/dist/omp/lifecycle.js.map +1 -1
  30. package/dist/omp/omb-sessions.d.ts +60 -3
  31. package/dist/omp/omb-sessions.d.ts.map +1 -1
  32. package/dist/omp/omb-sessions.js +183 -19
  33. package/dist/omp/omb-sessions.js.map +1 -1
  34. package/dist/omp/omb-settings.d.ts +17 -0
  35. package/dist/omp/omb-settings.d.ts.map +1 -1
  36. package/dist/omp/omb-settings.js +10 -0
  37. package/dist/omp/omb-settings.js.map +1 -1
  38. package/dist/omp/terminal-title.d.ts +70 -0
  39. package/dist/omp/terminal-title.d.ts.map +1 -0
  40. package/dist/omp/terminal-title.js +123 -0
  41. package/dist/omp/terminal-title.js.map +1 -0
  42. package/dist/omp/tmux.d.ts +58 -0
  43. package/dist/omp/tmux.d.ts.map +1 -1
  44. package/dist/omp/tmux.js +153 -7
  45. package/dist/omp/tmux.js.map +1 -1
  46. package/dist/storage/journal.d.ts +1 -1
  47. package/dist/storage/journal.d.ts.map +1 -1
  48. package/dist/storage/journal.js +2 -1
  49. package/dist/storage/journal.js.map +1 -1
  50. package/docs/compatibility/baseline.json +8 -7
  51. package/docs/compatibility/supported-surfaces.json +2 -1
  52. package/docs/releases/0.7.7.md +3 -0
  53. package/docs/releases/0.7.8.md +21 -0
  54. package/docs/releases/support-matrix.md +1 -1
  55. package/package.json +1 -1
  56. package/skills/bizar-omp/SKILL.md +24 -0
  57. package/skills/omp-native-development/SKILL.md +1 -1
  58. package/skills/omp-native-development/references/accuracy-and-versioning.md +1 -1
  59. package/skills/omp-native-development/references/source-manifest.json +3 -3
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Terminal tab title for a Bizar-owned session.
3
+ *
4
+ * OMP renders the tab title from its own session name, which for an `omb` host
5
+ * is the bare binary name — every tab reads "omb" and nothing distinguishes a
6
+ * working session from an idle one. `setTitle()` is the supported way to own
7
+ * that title, but an extension override is rendered verbatim: OMP's own
8
+ * run-state spinner never rewrites it. So the spinner has to be driven here,
9
+ * from the same turn boundaries that drive the hub's state.
10
+ *
11
+ * Frames are braille dots, the style OMP itself uses, so a Bizar title and a
12
+ * plain OMP title animate identically.
13
+ */
14
+ /** Braille spinner frames, matching OMP's own working-state animation. */
15
+ export declare const TITLE_SPINNER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
16
+ /** How often the working title advances a frame. */
17
+ export declare const TITLE_SPINNER_INTERVAL_MS = 120;
18
+ export type TitleState = "idle" | "working";
19
+ /**
20
+ * One rendered title, without any escape sequence.
21
+ *
22
+ * Kept pure so the exact text is testable without a terminal.
23
+ */
24
+ export declare function formatSessionTitle(input: {
25
+ label: string;
26
+ state: TitleState;
27
+ frame: number;
28
+ }): string;
29
+ /** Session identity sources a title label can be resolved from. */
30
+ export interface SessionTitleSources {
31
+ /** OMP's generated/renamed conversation title, once one exists. */
32
+ sessionName?: string | undefined;
33
+ /** The Bizar host name, which is a hashed directory id rather than a title. */
34
+ ombSessionName?: string | undefined;
35
+ /** The working directory, used to name the project before a title exists. */
36
+ cwd?: string | undefined;
37
+ }
38
+ /**
39
+ * The readable label for one session's tab.
40
+ *
41
+ * The host name is deliberately not used as the label: `bizar-<project>-<hash>`
42
+ * identifies a host, not a conversation, and putting it in the tab is exactly
43
+ * the unreadable title this replaces. OMP's session name is its conversation
44
+ * title once generated; before that the project directory names the work.
45
+ */
46
+ export declare function resolveSessionTitleLabel(sources: SessionTitleSources): string;
47
+ export interface SessionTitleController {
48
+ /** Point the title at a label, e.g. after the conversation is renamed. */
49
+ setLabel(label: string | undefined): void;
50
+ /** Enter or leave the working state; the spinner runs only while working. */
51
+ setState(state: TitleState): void;
52
+ /** Stop animating and leave the title in its last idle form. */
53
+ dispose(): void;
54
+ }
55
+ export interface SessionTitleOptions {
56
+ /** Emits one title update. Returns whether the write was accepted. */
57
+ write: (title: string) => void;
58
+ label?: string | undefined;
59
+ intervalMs?: number;
60
+ }
61
+ /**
62
+ * Drive a spinner-backed tab title.
63
+ *
64
+ * The timer is only armed while working: an idle session writes its title once
65
+ * and then stops, so a finished agent costs nothing. `write` is expected to be
66
+ * OMP's `ctx.ui.setTitle`, which is unavailable in non-interactive modes; the
67
+ * controller then simply does nothing rather than throwing on every frame.
68
+ */
69
+ export declare function createSessionTitleController(options: SessionTitleOptions): SessionTitleController;
70
+ //# sourceMappingURL=terminal-title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-title.d.ts","sourceRoot":"","sources":["../../src/omp/terminal-title.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB,YAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAEhG,oDAAoD;AACpD,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;AAM5C;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAMrG;AAED,mEAAmE;AACnE,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAc7E;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IAC1C,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAClC,gEAAgE;IAChE,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,sBAAsB,CAsDjG"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Terminal tab title for a Bizar-owned session.
3
+ *
4
+ * OMP renders the tab title from its own session name, which for an `omb` host
5
+ * is the bare binary name — every tab reads "omb" and nothing distinguishes a
6
+ * working session from an idle one. `setTitle()` is the supported way to own
7
+ * that title, but an extension override is rendered verbatim: OMP's own
8
+ * run-state spinner never rewrites it. So the spinner has to be driven here,
9
+ * from the same turn boundaries that drive the hub's state.
10
+ *
11
+ * Frames are braille dots, the style OMP itself uses, so a Bizar title and a
12
+ * plain OMP title animate identically.
13
+ */
14
+ /** Braille spinner frames, matching OMP's own working-state animation. */
15
+ export const TITLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
16
+ /** How often the working title advances a frame. */
17
+ export const TITLE_SPINNER_INTERVAL_MS = 120;
18
+ /** Separator between the spinner and the session label, mirroring OMP. */
19
+ const WORKING_SEPARATOR = ":";
20
+ const IDLE_SEPARATOR = ">";
21
+ /**
22
+ * One rendered title, without any escape sequence.
23
+ *
24
+ * Kept pure so the exact text is testable without a terminal.
25
+ */
26
+ export function formatSessionTitle(input) {
27
+ const label = input.label.trim();
28
+ if (!label)
29
+ return "";
30
+ if (input.state !== "working")
31
+ return `${label} ${IDLE_SEPARATOR}`;
32
+ const frame = TITLE_SPINNER_FRAMES[((input.frame % TITLE_SPINNER_FRAMES.length) + TITLE_SPINNER_FRAMES.length) % TITLE_SPINNER_FRAMES.length];
33
+ return `${frame} ${WORKING_SEPARATOR} ${label}`;
34
+ }
35
+ /**
36
+ * The readable label for one session's tab.
37
+ *
38
+ * The host name is deliberately not used as the label: `bizar-<project>-<hash>`
39
+ * identifies a host, not a conversation, and putting it in the tab is exactly
40
+ * the unreadable title this replaces. OMP's session name is its conversation
41
+ * title once generated; before that the project directory names the work.
42
+ */
43
+ export function resolveSessionTitleLabel(sources) {
44
+ const title = sources.sessionName?.trim();
45
+ if (title && title !== "omb")
46
+ return title;
47
+ const cwd = sources.cwd?.trim();
48
+ if (cwd) {
49
+ const base = cwd.replace(/[\\/]+$/, "").split(/[\\/]/).pop();
50
+ if (base)
51
+ return base;
52
+ }
53
+ // Last resort: strip the hash off the Bizar host name, which still names the
54
+ // project even when no directory or title is available. A bare "omb" is the
55
+ // unreadable title this whole path exists to replace, so it is never used.
56
+ const host = sources.ombSessionName?.trim();
57
+ if (host)
58
+ return host.replace(/^bizar-/, "").replace(/-[a-f0-9]{10}(?:-.*)?$/, "") || host;
59
+ return "";
60
+ }
61
+ /**
62
+ * Drive a spinner-backed tab title.
63
+ *
64
+ * The timer is only armed while working: an idle session writes its title once
65
+ * and then stops, so a finished agent costs nothing. `write` is expected to be
66
+ * OMP's `ctx.ui.setTitle`, which is unavailable in non-interactive modes; the
67
+ * controller then simply does nothing rather than throwing on every frame.
68
+ */
69
+ export function createSessionTitleController(options) {
70
+ const intervalMs = options.intervalMs ?? TITLE_SPINNER_INTERVAL_MS;
71
+ let label = options.label ?? "";
72
+ let state = "idle";
73
+ let frame = 0;
74
+ let timer;
75
+ const render = () => formatSessionTitle({ label, state, frame });
76
+ const stopTimer = () => {
77
+ if (timer)
78
+ clearInterval(timer);
79
+ timer = undefined;
80
+ };
81
+ const emit = () => {
82
+ const title = render();
83
+ // An empty title would release OMP's override and strand the tab on the
84
+ // bare brand, so a nameless session writes nothing at all.
85
+ if (!title)
86
+ return;
87
+ options.write(title);
88
+ };
89
+ const startTimer = () => {
90
+ if (timer)
91
+ return;
92
+ timer = setInterval(() => {
93
+ frame += 1;
94
+ emit();
95
+ }, intervalMs);
96
+ // A spinner must never be the reason the process stays alive.
97
+ timer.unref?.();
98
+ };
99
+ return {
100
+ setLabel(next) {
101
+ label = next ?? "";
102
+ emit();
103
+ },
104
+ setState(next) {
105
+ if (state === next)
106
+ return;
107
+ state = next;
108
+ if (state === "working") {
109
+ frame = 0;
110
+ emit();
111
+ startTimer();
112
+ return;
113
+ }
114
+ stopTimer();
115
+ frame = 0;
116
+ emit();
117
+ },
118
+ dispose() {
119
+ stopTimer();
120
+ },
121
+ };
122
+ }
123
+ //# sourceMappingURL=terminal-title.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-title.js","sourceRoot":"","sources":["../../src/omp/terminal-title.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,42 @@ export declare function shouldLaunchOmbInTmux(options?: TmuxLaunchEnvironment):
11
11
  * A session id creates an additional durable session in the same project.
12
12
  */
13
13
  export declare function ombSessionName(cwd: string, sessionId?: string, profile?: string): string;
14
+ /**
15
+ * Re-point a live host at the generation an operator explicitly switched to.
16
+ *
17
+ * `bindNativeSessionId` writes the generation immutably on purpose: an
18
+ * unattended dead-pane recovery must never be talked onto a different journal.
19
+ * This is the one sanctioned exception, and it differs in three ways. The
20
+ * caller is an explicit `/resume`, not a recovery. The new generation is
21
+ * supplied, never inferred from whatever journal is newest in a shared project
22
+ * directory. And the previous value is returned for reporting.
23
+ *
24
+ * The previous value is deliberately *not* meant to be written back. OMP has
25
+ * already switched by the time this runs, so restoring the old generation would
26
+ * make `assertNativeSessionBinding` pass against the conversation this host has
27
+ * left — a silent divergence. A caller that cannot finish the switch must
28
+ * instead call {@link clearOmbHostGeneration} or drop the journal manifest, so
29
+ * the respawn check refuses.
30
+ */
31
+ export declare function rebindOmbHostGeneration(session: string, nativeId: string, cwd?: string, env?: NodeJS.ProcessEnv): Promise<{
32
+ ok: true;
33
+ previous?: string;
34
+ } | {
35
+ ok: false;
36
+ reason: string;
37
+ previous?: string;
38
+ }>;
39
+ /**
40
+ * Clear a host's generation binding so a later respawn is refused.
41
+ *
42
+ * Used when OMP has already switched conversations but the host's records
43
+ * could not be brought into agreement. Restoring the *previous* generation
44
+ * would be actively unsafe: the respawn check would then pass against the old
45
+ * conversation while the host writes the new one. An absent binding always
46
+ * fails that check, so the respawn stops instead of resuming the wrong
47
+ * journal, and the operator is told to restart the host.
48
+ */
49
+ export declare function clearOmbHostGeneration(session: string, cwd?: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
14
50
  /** Candidate command names in lookup order, kept pure for cross-platform tests. */
15
51
  export declare function executableCandidates(bin: string, platform: "win32" | "posix", pathext?: string): string[];
16
52
  /** Resolve an executable on `$PATH`, or an explicit path when it contains a slash. */
@@ -99,4 +135,26 @@ export declare function createOmbSession(cwd: string, args: string[], env?: Node
99
135
  name: string;
100
136
  code: number;
101
137
  }>;
138
+ /**
139
+ * Recreate a host for a stopped session, bound to the journal it already wrote.
140
+ *
141
+ * This is the only route back into a stopped conversation: attaching is
142
+ * impossible while nothing hosts it. `nativeId` must be the exact generation
143
+ * the selected row reported — a project directory holds every session for
144
+ * that project, so resolving "the newest journal" here would resume a
145
+ * different conversation than the one the operator chose. A row without a
146
+ * known generation is refused rather than guessed at.
147
+ *
148
+ * A session whose journal still sits in a Bizar private directory is migrated
149
+ * into OMP's project directory first: that is the directory the new host is
150
+ * started with, and `--resume` can only find a journal there. The migration is
151
+ * verified against the requested id, so a host is never started on a journal
152
+ * belonging to another session. The binding invariant is untouched — the fresh
153
+ * host records the same generation it always did, and a mismatched one still
154
+ * refuses.
155
+ */
156
+ export declare function continueOmbSession(session: string, cwd: string, nativeId: string | undefined, env?: NodeJS.ProcessEnv): Promise<{
157
+ name: string;
158
+ code: number;
159
+ }>;
102
160
  //# sourceMappingURL=tmux.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tmux.d.ts","sourceRoot":"","sources":["../../src/omp/tmux.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tmux.d.ts","sourceRoot":"","sources":["../../src/omp/tmux.ts"],"names":[],"mappings":"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"}
package/dist/omp/tmux.js CHANGED
@@ -99,8 +99,15 @@ function quiesceStdinForHandoff() {
99
99
  }
100
100
  const NATIVE_ID_OPTION = "@bizar-native-id";
101
101
  const NATIVE_OWNER_OPTION = "@bizar-session-owner";
102
- /** Read the native session header id from the exact directory a pane owns. */
103
- async function nativeSessionId(sessionDir) {
102
+ /**
103
+ * Read the native session header id from the exact directory a pane owns.
104
+ *
105
+ * `only` narrows the answer to one generation. A project directory holds every
106
+ * session for that project, so the newest journal is not necessarily the one an
107
+ * operator selected; a caller that must resume a specific conversation passes
108
+ * its id and gets `undefined` unless that exact journal is present.
109
+ */
110
+ async function nativeSessionId(sessionDir, only) {
104
111
  const journals = await listSessionJournals(sessionDir);
105
112
  for (const journal of journals.toReversed()) {
106
113
  const head = await readJournalHead(journal);
@@ -109,8 +116,11 @@ async function nativeSessionId(sessionDir) {
109
116
  for (const line of head.split("\n")) {
110
117
  try {
111
118
  const entry = JSON.parse(line);
112
- if (entry.type === "session" && typeof entry.id === "string" && entry.id.trim())
113
- return entry.id.trim();
119
+ if (entry.type !== "session" || typeof entry.id !== "string" || !entry.id.trim())
120
+ continue;
121
+ const id = entry.id.trim();
122
+ if (!only || id === only || id.startsWith(only))
123
+ return id;
114
124
  }
115
125
  catch { /* a partial journal line is not a native session header */ }
116
126
  }
@@ -121,21 +131,31 @@ async function nativeSessionId(sessionDir) {
121
131
  * Bind a tmux session to the native journal generation it started. Once set,
122
132
  * the option is never replaced: a conflicting journal is an unsafe recovery,
123
133
  * not an invitation to repoint the tmux host.
134
+ *
135
+ * `expectedId` pins the generation for a host that is being restarted onto a
136
+ * specific conversation. Without it the id would be resolved from whatever
137
+ * journal happens to be newest in the directory, which in a shared project
138
+ * bucket is a different session than the operator chose.
124
139
  */
125
- async function bindNativeSessionId(tmux, session, sessionDir, cwd, childEnv, existingSession = false) {
140
+ async function bindNativeSessionId(tmux, session, sessionDir, cwd, childEnv, existingSession = false, expectedId) {
126
141
  const legacyPrivateDir = resolve(sessionDir) === resolve(ombSessionDir(session, childEnv));
127
- let nativeId = ombNativeSessionReference(session, childEnv)?.id ?? (legacyPrivateDir ? await nativeSessionId(sessionDir) : undefined);
142
+ let nativeId = expectedId
143
+ ?? ombNativeSessionReference(session, childEnv)?.id
144
+ ?? (legacyPrivateDir ? await nativeSessionId(sessionDir) : undefined);
128
145
  if (!nativeId && existingSession) {
129
146
  const pane = await readTmux(tmux, ["list-panes", "-t", `${session}:0.0`, "-F", "#{pane_start_command}"], cwd, childEnv);
130
147
  const paneDir = pane.code === 0 ? sessionDirFromPaneCommand(pane.output.trim()) : undefined;
131
148
  if (paneDir && (resolve(paneDir) === resolve(ombSessionDir(session, childEnv)) || (await listSessionJournals(paneDir)).length === 1)) {
132
- nativeId = await nativeSessionId(paneDir);
149
+ nativeId = await nativeSessionId(paneDir, expectedId);
133
150
  }
134
151
  }
135
152
  // A newly started OMP can still be creating its first journal. Its next
136
153
  // launch records the binding; a dead pane without a binding is refused.
137
154
  if (!nativeId)
138
155
  return 0;
156
+ if (expectedId && !expectedId.startsWith(nativeId) && !nativeId.startsWith(expectedId)) {
157
+ throw new Error(`Bizar tmux session ${session} would bind native generation ${nativeId} instead of the requested ${expectedId}`);
158
+ }
139
159
  const stored = await readTmux(tmux, ["show-options", "-t", session, "-v", NATIVE_ID_OPTION], cwd, childEnv);
140
160
  if (stored.code === 0 && stored.output.trim()) {
141
161
  if (stored.output.trim() !== nativeId) {
@@ -161,6 +181,71 @@ async function assertNativeSessionBinding(tmux, session, sessionDir, cwd, childE
161
181
  throw new Error(`Refusing to respawn ${session}: its tmux native session generation does not match ${sessionDir}`);
162
182
  }
163
183
  }
184
+ /** The generation a host is currently bound to, reported to the operator. */
185
+ async function readNativeSessionBinding(tmux, session, cwd, childEnv) {
186
+ const stored = await readTmux(tmux, ["show-options", "-t", session, "-v", NATIVE_ID_OPTION], cwd, childEnv);
187
+ const value = stored.output.trim();
188
+ return stored.code === 0 && value ? value : undefined;
189
+ }
190
+ /**
191
+ * Re-point a live host at the generation an operator explicitly switched to.
192
+ *
193
+ * `bindNativeSessionId` writes the generation immutably on purpose: an
194
+ * unattended dead-pane recovery must never be talked onto a different journal.
195
+ * This is the one sanctioned exception, and it differs in three ways. The
196
+ * caller is an explicit `/resume`, not a recovery. The new generation is
197
+ * supplied, never inferred from whatever journal is newest in a shared project
198
+ * directory. And the previous value is returned for reporting.
199
+ *
200
+ * The previous value is deliberately *not* meant to be written back. OMP has
201
+ * already switched by the time this runs, so restoring the old generation would
202
+ * make `assertNativeSessionBinding` pass against the conversation this host has
203
+ * left — a silent divergence. A caller that cannot finish the switch must
204
+ * instead call {@link clearOmbHostGeneration} or drop the journal manifest, so
205
+ * the respawn check refuses.
206
+ */
207
+ export async function rebindOmbHostGeneration(session, nativeId, cwd = process.cwd(), env = process.env) {
208
+ if (!isOmbSessionName(session))
209
+ return { ok: false, reason: "not a Bizar session name" };
210
+ const id = nativeId.trim();
211
+ if (!id)
212
+ return { ok: false, reason: "no native session generation was supplied" };
213
+ const tmux = resolveMultiplexerBin(env);
214
+ const childEnv = { ...env, BIZAR_OMP_NO_TMUX: "1" };
215
+ const previous = await readNativeSessionBinding(tmux, session, cwd, childEnv);
216
+ if (previous === id)
217
+ return { ok: true, previous };
218
+ // No `-o`: this replaces an immutable value on purpose, and only here.
219
+ const set = await runTmux(tmux, ["set-option", "-t", session, NATIVE_ID_OPTION, id], cwd, childEnv);
220
+ if (set !== 0)
221
+ return { ok: false, reason: `multiplexer refused the rebind (exit ${set})`, previous };
222
+ const confirmed = await readNativeSessionBinding(tmux, session, cwd, childEnv);
223
+ if (confirmed !== id)
224
+ return { ok: false, reason: "the rebind did not take effect", previous };
225
+ return { ok: true, previous };
226
+ }
227
+ /**
228
+ * Clear a host's generation binding so a later respawn is refused.
229
+ *
230
+ * Used when OMP has already switched conversations but the host's records
231
+ * could not be brought into agreement. Restoring the *previous* generation
232
+ * would be actively unsafe: the respawn check would then pass against the old
233
+ * conversation while the host writes the new one. An absent binding always
234
+ * fails that check, so the respawn stops instead of resuming the wrong
235
+ * journal, and the operator is told to restart the host.
236
+ */
237
+ export async function clearOmbHostGeneration(session, cwd = process.cwd(), env = process.env) {
238
+ if (!isOmbSessionName(session))
239
+ return false;
240
+ const tmux = resolveMultiplexerBin(env);
241
+ const childEnv = { ...env, BIZAR_OMP_NO_TMUX: "1" };
242
+ const cleared = await runTmux(tmux, ["set-option", "-u", "-t", session, NATIVE_ID_OPTION], cwd, childEnv);
243
+ if (cleared !== 0)
244
+ return false;
245
+ // Confirm the invalidation landed: a binding that survived would let the
246
+ // respawn check pass against a journal this host no longer writes.
247
+ return (await readNativeSessionBinding(tmux, session, cwd, childEnv)) === undefined;
248
+ }
164
249
  async function bindNativeSessionOwner(tmux, session, cwd, childEnv, owner = session) {
165
250
  const stored = await readTmux(tmux, ["show-options", "-t", session, "-v", NATIVE_OWNER_OPTION], cwd, childEnv);
166
251
  if (stored.code === 0 && stored.output.trim())
@@ -712,4 +797,65 @@ export async function createOmbSession(cwd, args, env = process.env, sessionId)
712
797
  clearOmbSessionDeleted(session, env);
713
798
  return { name: session, code: preparedCode };
714
799
  }
800
+ /**
801
+ * Recreate a host for a stopped session, bound to the journal it already wrote.
802
+ *
803
+ * This is the only route back into a stopped conversation: attaching is
804
+ * impossible while nothing hosts it. `nativeId` must be the exact generation
805
+ * the selected row reported — a project directory holds every session for
806
+ * that project, so resolving "the newest journal" here would resume a
807
+ * different conversation than the one the operator chose. A row without a
808
+ * known generation is refused rather than guessed at.
809
+ *
810
+ * A session whose journal still sits in a Bizar private directory is migrated
811
+ * into OMP's project directory first: that is the directory the new host is
812
+ * started with, and `--resume` can only find a journal there. The migration is
813
+ * verified against the requested id, so a host is never started on a journal
814
+ * belonging to another session. The binding invariant is untouched — the fresh
815
+ * host records the same generation it always did, and a mismatched one still
816
+ * refuses.
817
+ */
818
+ export async function continueOmbSession(session, cwd, nativeId, env = process.env) {
819
+ if (!isOmbSessionName(session))
820
+ return { name: session, code: 2 };
821
+ const id = nativeId?.trim();
822
+ if (!id)
823
+ return { name: session, code: 3 };
824
+ const tmux = resolveMultiplexerBin(env);
825
+ const omp = env.BIZAR_OMP_BIN || "omp";
826
+ const settings = loadOmbSettings(env);
827
+ const childEnv = { ...env, BIZAR_OMP_NO_TMUX: "1" };
828
+ const journalDir = defaultOmpSessionDir(cwd, env);
829
+ // The journal must be reachable from the directory the host will search.
830
+ // A legacy private journal is handed to OMP's own picker directory first.
831
+ let resolvable = await nativeSessionId(journalDir, id);
832
+ if (!resolvable && isLegacyOmbSessionDir(ombSessionDir(session, env), session, env)) {
833
+ const migrated = await migrateLegacyOmbSessionJournal(session, ombSessionDir(session, env), cwd, env);
834
+ if (!migrated.ok)
835
+ return { name: session, code: 6 };
836
+ if (migrated.id !== id)
837
+ return { name: session, code: 7 };
838
+ resolvable = await nativeSessionId(journalDir, id);
839
+ }
840
+ // Fail closed unless the journal OMP would resume is really in the directory
841
+ // the host will look in; otherwise `--resume` silently starts a new session.
842
+ if (!resolvable)
843
+ return { name: session, code: 5 };
844
+ const hasSession = await runTmux(tmux, ["has-session", "-t", session], cwd, childEnv);
845
+ if (hasSession === 0)
846
+ return { name: session, code: 4 };
847
+ const created = await runTmux(tmux, ["new-session", "-d", "-s", session, "-e", `BIZAR_OMP_SESSION_NAME=${session}`, "-c", cwd, omp, "--session-dir", journalDir, "--resume", id], cwd, childEnv);
848
+ if (created !== 0)
849
+ return { name: session, code: created };
850
+ const ownerBound = await bindNativeSessionOwner(tmux, session, cwd, childEnv);
851
+ if (ownerBound !== 0)
852
+ return { name: session, code: ownerBound };
853
+ const bound = await bindNativeSessionId(tmux, session, journalDir, cwd, childEnv, false, id);
854
+ if (bound !== 0)
855
+ return { name: session, code: bound };
856
+ const prepared = await prepareOmbSession(tmux, omp, session, cwd, childEnv, settings.mouseScrolling);
857
+ if (prepared === 0)
858
+ clearOmbSessionDeleted(session, env);
859
+ return { name: session, code: prepared };
860
+ }
715
861
  //# sourceMappingURL=tmux.js.map