pi-tmux-session-map 0.3.0 → 0.5.0

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.
@@ -1,4 +1,4 @@
1
- export type SessionMapping = {
1
+ export type SessionMappingV2 = {
2
2
  schema: 2;
3
3
  paneKey: string;
4
4
  sessionFile: string;
@@ -8,16 +8,52 @@ export type SessionMapping = {
8
8
  updatedAtMs: number;
9
9
  };
10
10
 
11
+ export type SessionMapping = {
12
+ schema: 3;
13
+ paneKey: string;
14
+ paneId: string;
15
+ tmuxServerPid: string;
16
+ sessionFile: string;
17
+ sessionId: string;
18
+ cwd: string;
19
+ sessionName: string | null;
20
+ updatedAtMs: number;
21
+ };
22
+
23
+ export type StructuredSessionMapping = SessionMappingV2 | SessionMapping;
24
+
11
25
  export type ParsedSessionMapping =
12
- | { format: "structured"; mapping: SessionMapping }
26
+ | { format: "structured"; mapping: StructuredSessionMapping }
13
27
  | { format: "legacy"; sessionFile: string };
14
28
 
15
29
  function isRecord(value: unknown): value is Record<string, unknown> {
16
30
  return typeof value === "object" && value !== null && !Array.isArray(value);
17
31
  }
18
32
 
33
+ type CommonMappingFields = {
34
+ paneKey: string;
35
+ sessionFile: string;
36
+ sessionId: string;
37
+ cwd: string;
38
+ sessionName: string | null;
39
+ updatedAtMs: number;
40
+ };
41
+
42
+ function hasCommonFields(value: Record<string, unknown>): value is Record<string, unknown> & CommonMappingFields {
43
+ return (
44
+ typeof value.paneKey === "string"
45
+ && typeof value.sessionFile === "string"
46
+ && typeof value.sessionId === "string"
47
+ && typeof value.cwd === "string"
48
+ && (typeof value.sessionName === "string" || value.sessionName === null)
49
+ && typeof value.updatedAtMs === "number"
50
+ );
51
+ }
52
+
19
53
  export function buildSessionMapping(
20
54
  paneKey: string,
55
+ paneId: string,
56
+ tmuxServerPid: string,
21
57
  sessionFile: string,
22
58
  sessionId: string,
23
59
  cwd: string,
@@ -25,8 +61,10 @@ export function buildSessionMapping(
25
61
  updatedAtMs: number,
26
62
  ): SessionMapping {
27
63
  return {
28
- schema: 2,
64
+ schema: 3,
29
65
  paneKey,
66
+ paneId,
67
+ tmuxServerPid,
30
68
  sessionFile,
31
69
  sessionId,
32
70
  cwd,
@@ -54,30 +92,46 @@ export function parseSessionMapping(content: string): ParsedSessionMapping | und
54
92
  } catch {
55
93
  return undefined;
56
94
  }
95
+ if (!isRecord(value) || !hasCommonFields(value)) {
96
+ return undefined;
97
+ }
98
+ if (value.schema === 2) {
99
+ return {
100
+ format: "structured",
101
+ mapping: {
102
+ schema: 2,
103
+ paneKey: value.paneKey,
104
+ sessionFile: value.sessionFile,
105
+ sessionId: value.sessionId,
106
+ cwd: value.cwd,
107
+ sessionName: value.sessionName,
108
+ updatedAtMs: value.updatedAtMs,
109
+ },
110
+ };
111
+ }
57
112
  if (
58
- !isRecord(value)
59
- || value.schema !== 2
60
- || typeof value.paneKey !== "string"
61
- || typeof value.sessionFile !== "string"
62
- || typeof value.sessionId !== "string"
63
- || typeof value.cwd !== "string"
64
- || !(typeof value.sessionName === "string" || value.sessionName === null)
65
- || typeof value.updatedAtMs !== "number"
113
+ value.schema === 3
114
+ && typeof value.paneId === "string"
115
+ && value.paneId !== ""
116
+ && typeof value.tmuxServerPid === "string"
117
+ && value.tmuxServerPid !== ""
66
118
  ) {
67
- return undefined;
119
+ return {
120
+ format: "structured",
121
+ mapping: {
122
+ schema: 3,
123
+ paneKey: value.paneKey,
124
+ paneId: value.paneId,
125
+ tmuxServerPid: value.tmuxServerPid,
126
+ sessionFile: value.sessionFile,
127
+ sessionId: value.sessionId,
128
+ cwd: value.cwd,
129
+ sessionName: value.sessionName,
130
+ updatedAtMs: value.updatedAtMs,
131
+ },
132
+ };
68
133
  }
69
- return {
70
- format: "structured",
71
- mapping: {
72
- schema: 2,
73
- paneKey: value.paneKey,
74
- sessionFile: value.sessionFile,
75
- sessionId: value.sessionId,
76
- cwd: value.cwd,
77
- sessionName: value.sessionName,
78
- updatedAtMs: value.updatedAtMs,
79
- },
80
- };
134
+ return undefined;
81
135
  }
82
136
 
83
137
  export function mappedSessionFile(content: string): string | undefined {
@@ -87,3 +141,14 @@ export function mappedSessionFile(content: string): string | undefined {
87
141
  }
88
142
  return parsed.format === "structured" ? parsed.mapping.sessionFile : parsed.sessionFile;
89
143
  }
144
+
145
+ export function mappedPaneIdentity(content: string): { paneId: string; tmuxServerPid: string } | undefined {
146
+ const parsed = parseSessionMapping(content);
147
+ if (!parsed || parsed.format !== "structured" || parsed.mapping.schema !== 3) {
148
+ return undefined;
149
+ }
150
+ return {
151
+ paneId: parsed.mapping.paneId,
152
+ tmuxServerPid: parsed.mapping.tmuxServerPid,
153
+ };
154
+ }
@@ -0,0 +1,56 @@
1
+ // Records whether this process received SIGHUP or SIGTERM, so a session_shutdown
2
+ // caused by a closed terminal, a dead tmux server or a reboot can be told apart
3
+ // from an interactive quit (both reach extensions as reason "quit").
4
+ //
5
+ // Pi's own handler starts the shutdown from the same signal dispatch. The
6
+ // listener here only sets a flag; consumers read it later, when the queued
7
+ // status write builds its payload: Node runs every signal listener
8
+ // synchronously before any promise continuation, so the flag is set by then
9
+ // regardless of listener order.
10
+
11
+ export type TerminationSignal = "SIGHUP" | "SIGTERM";
12
+
13
+ type SignalTarget = {
14
+ prependListener(event: TerminationSignal, listener: () => void): unknown;
15
+ removeListener(event: TerminationSignal, listener: () => void): unknown;
16
+ };
17
+
18
+ let recorded: TerminationSignal | null = null;
19
+
20
+ export function recordedTerminationSignal(): TerminationSignal | null {
21
+ return recorded;
22
+ }
23
+
24
+ /** Test hook: forget a recorded signal. */
25
+ export function resetRecordedTerminationSignal(): void {
26
+ recorded = null;
27
+ }
28
+
29
+ export type SignalEvidenceListener = {
30
+ install(): void;
31
+ uninstall(): void;
32
+ };
33
+
34
+ export function createSignalEvidenceListener(target: SignalTarget = process): SignalEvidenceListener {
35
+ const listeners = new Map<TerminationSignal, () => void>();
36
+ return {
37
+ install() {
38
+ if (listeners.size > 0) {
39
+ return;
40
+ }
41
+ for (const signal of ["SIGHUP", "SIGTERM"] as const) {
42
+ const listener = () => {
43
+ recorded ??= signal;
44
+ };
45
+ listeners.set(signal, listener);
46
+ target.prependListener(signal, listener);
47
+ }
48
+ },
49
+ uninstall() {
50
+ for (const [signal, listener] of listeners) {
51
+ target.removeListener(signal, listener);
52
+ }
53
+ listeners.clear();
54
+ },
55
+ };
56
+ }
@@ -4,6 +4,7 @@ export type PaneInfo = {
4
4
  backend: "tmux" | "zellij" | "mex";
5
5
  paneKey: string;
6
6
  paneId: string;
7
+ tmuxServerPid: string | null;
7
8
  sessionName: string;
8
9
  windowIndex: number;
9
10
  paneIndex: number;
@@ -15,6 +16,44 @@ export type SessionInfo = {
15
16
  sessionName: string | null;
16
17
  };
17
18
 
19
+ export type LiveSessionIdentity = {
20
+ sessionId: string;
21
+ piPid: number;
22
+ };
23
+
24
+ /** Leaf before the run's prompt entry and the run start (agent_start). */
25
+ export type RunAnchor = {
26
+ leaf_id: string | null;
27
+ started_at_ms: number;
28
+ };
29
+
30
+ /**
31
+ * Why a session_shutdown happened: a SIGHUP/SIGTERM (terminal closed, tmux server
32
+ * died, reboot), an interactive quit, a reload, or a session replacement.
33
+ */
34
+ export type ShutdownCause = "signal" | "quit" | "reload" | "replace";
35
+
36
+ /**
37
+ * Auto-continue state of this Pi for displays (TWS). `state`: planned, dispatched, admitted, an
38
+ * outcome (done, failed, cancelled, …), uncertain, skipped or stopped; `reason` for skipped and
39
+ * stopped. Slot and deadline come from the released plan; `key` and `ledger_dir` name the ledger
40
+ * entry, where TWS may veto a planned continuation with `<key>.skip`.
41
+ */
42
+ export type ContinueStatus = {
43
+ state: string;
44
+ reason: string | null;
45
+ slot_ms: number | null;
46
+ deadline_ms: number | null;
47
+ key: string;
48
+ ledger_dir: string;
49
+ };
50
+
51
+ export type RecoveryEvidence = {
52
+ runAnchor: RunAnchor | null;
53
+ shutdownCause: ShutdownCause | null;
54
+ continueState?: ContinueStatus | null;
55
+ };
56
+
18
57
  export type TwsStatusPayload = {
19
58
  schema: 1;
20
59
  agent: "pi";
@@ -27,10 +66,18 @@ export type TwsStatusPayload = {
27
66
  cwd: string;
28
67
  session_file: string | null;
29
68
  session_name: string | null;
69
+ // Additive live identity: valid even before the session JSONL exists.
70
+ session_id: string;
71
+ pi_pid: number;
72
+ tmux_server_pid: string | null;
30
73
  state: WorkState;
31
74
  updated_at_ms: number;
32
75
  started_at_ms: number | null;
33
76
  finished_at_ms: number | null;
77
+ // Additive recovery evidence for tmux-recovery's auto-continue decision.
78
+ run_anchor: RunAnchor | null;
79
+ shutdown_cause: ShutdownCause | null;
80
+ continue?: ContinueStatus;
34
81
  };
35
82
 
36
83
  export function buildTwsStatusPayload(
@@ -40,6 +87,8 @@ export function buildTwsStatusPayload(
40
87
  updatedAtMs: number,
41
88
  startedAtMs: number | null,
42
89
  finishedAtMs: number | null,
90
+ identity: LiveSessionIdentity,
91
+ evidence: RecoveryEvidence = { runAnchor: null, shutdownCause: null },
43
92
  ): TwsStatusPayload {
44
93
  return {
45
94
  schema: 1,
@@ -53,10 +102,16 @@ export function buildTwsStatusPayload(
53
102
  cwd: session.cwd,
54
103
  session_file: session.sessionFile,
55
104
  session_name: session.sessionName,
105
+ session_id: identity.sessionId,
106
+ pi_pid: identity.piPid,
107
+ tmux_server_pid: pane.tmuxServerPid,
56
108
  state,
57
109
  updated_at_ms: updatedAtMs,
58
110
  started_at_ms: startedAtMs,
59
111
  finished_at_ms: finishedAtMs,
112
+ run_anchor: evidence.runAnchor,
113
+ shutdown_cause: state === "shutdown" ? evidence.shutdownCause : null,
114
+ ...(evidence.continueState ? { continue: evidence.continueState } : {}),
60
115
  };
61
116
  }
62
117