aicodeman 1.29.0 → 1.30.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.
Files changed (141) hide show
  1. package/README.md +57 -30
  2. package/README.zh-CN.md +207 -51
  3. package/dist/reboot-restore.d.ts +146 -0
  4. package/dist/reboot-restore.d.ts.map +1 -0
  5. package/dist/reboot-restore.js +205 -0
  6. package/dist/reboot-restore.js.map +1 -0
  7. package/dist/session-auto-name.d.ts +74 -0
  8. package/dist/session-auto-name.d.ts.map +1 -0
  9. package/dist/session-auto-name.js +339 -0
  10. package/dist/session-auto-name.js.map +1 -0
  11. package/dist/session-env-clamp.d.ts +80 -0
  12. package/dist/session-env-clamp.d.ts.map +1 -0
  13. package/dist/session-env-clamp.js +96 -0
  14. package/dist/session-env-clamp.js.map +1 -0
  15. package/dist/session.d.ts +54 -4
  16. package/dist/session.d.ts.map +1 -1
  17. package/dist/session.js +86 -7
  18. package/dist/session.js.map +1 -1
  19. package/dist/types/session.d.ts +18 -0
  20. package/dist/types/session.d.ts.map +1 -1
  21. package/dist/types/session.js.map +1 -1
  22. package/dist/web/ports/session-port.d.ts +31 -0
  23. package/dist/web/ports/session-port.d.ts.map +1 -1
  24. package/dist/web/public/admin-ui.js.gz +0 -0
  25. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  26. package/dist/web/public/{app.556be563.js → app.6d2dc4e8.js} +2 -2
  27. package/dist/web/public/app.6d2dc4e8.js.br +0 -0
  28. package/dist/web/public/app.6d2dc4e8.js.gz +0 -0
  29. package/dist/web/public/approvals-ui.js.gz +0 -0
  30. package/dist/web/public/constants.258b140f.js.gz +0 -0
  31. package/dist/web/public/cron-ui.js.gz +0 -0
  32. package/dist/web/public/entrance-animations.js.gz +0 -0
  33. package/dist/web/public/home-sessions.js.gz +0 -0
  34. package/dist/web/public/i18n.5f897ed5.js +1 -0
  35. package/dist/web/public/i18n.5f897ed5.js.br +0 -0
  36. package/dist/web/public/i18n.5f897ed5.js.gz +0 -0
  37. package/dist/web/public/image-input.cd4b97c4.js.gz +0 -0
  38. package/dist/web/public/index.html +34 -8
  39. package/dist/web/public/index.html.br +0 -0
  40. package/dist/web/public/index.html.gz +0 -0
  41. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  42. package/dist/web/public/keyboard-accessory.2cf04f17.js.gz +0 -0
  43. package/dist/web/public/mobile-handlers.6f354a87.js.gz +0 -0
  44. package/dist/web/public/mobile-overview.js.gz +0 -0
  45. package/dist/web/public/{mobile.e9d0b53e.css → mobile.6caaa28a.css} +1 -1
  46. package/dist/web/public/mobile.6caaa28a.css.br +0 -0
  47. package/dist/web/public/mobile.6caaa28a.css.gz +0 -0
  48. package/dist/web/public/notification-manager.36ea4624.js.gz +0 -0
  49. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  50. package/dist/web/public/panels-ui.5b07ad14.js.gz +0 -0
  51. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  52. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  53. package/dist/web/public/readmymind-ui.js.gz +0 -0
  54. package/dist/web/public/reboot-restore-ui.js +142 -0
  55. package/dist/web/public/reboot-restore-ui.js.br +0 -0
  56. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  57. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  58. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  59. package/dist/web/public/session-lineage.js.gz +0 -0
  60. package/dist/web/public/session-ui.42b81477.js.gz +0 -0
  61. package/dist/web/public/settings-ui.58f1d756.js +67 -0
  62. package/dist/web/public/settings-ui.58f1d756.js.br +0 -0
  63. package/dist/web/public/settings-ui.58f1d756.js.gz +0 -0
  64. package/dist/web/public/styles.e6edfb0b.css +1 -0
  65. package/dist/web/public/styles.e6edfb0b.css.br +0 -0
  66. package/dist/web/public/styles.e6edfb0b.css.gz +0 -0
  67. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  68. package/dist/web/public/sw.js.gz +0 -0
  69. package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
  70. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js +1 -0
  71. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.br +0 -0
  72. package/dist/web/public/terminal-keycode229-recovery.fb91b25b.js.gz +0 -0
  73. package/dist/web/public/{terminal-ui.38c49244.js → terminal-ui.4f8d5820.js} +2 -2
  74. package/dist/web/public/terminal-ui.4f8d5820.js.br +0 -0
  75. package/dist/web/public/terminal-ui.4f8d5820.js.gz +0 -0
  76. package/dist/web/public/ultracode-panel.js.gz +0 -0
  77. package/dist/web/public/ultracode-windows.js.gz +0 -0
  78. package/dist/web/public/upload.html.gz +0 -0
  79. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  80. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  81. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  82. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  83. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  84. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  85. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  86. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  87. package/dist/web/public/vendor/xterm.css.gz +0 -0
  88. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  89. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
  90. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  91. package/dist/web/public/webview-tabs.js.gz +0 -0
  92. package/dist/web/reboot-restore-registry.d.ts +107 -0
  93. package/dist/web/reboot-restore-registry.d.ts.map +1 -0
  94. package/dist/web/reboot-restore-registry.js +189 -0
  95. package/dist/web/reboot-restore-registry.js.map +1 -0
  96. package/dist/web/routes/index.d.ts +1 -0
  97. package/dist/web/routes/index.d.ts.map +1 -1
  98. package/dist/web/routes/index.js +1 -0
  99. package/dist/web/routes/index.js.map +1 -1
  100. package/dist/web/routes/reboot-restore-routes.d.ts +30 -0
  101. package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -0
  102. package/dist/web/routes/reboot-restore-routes.js +273 -0
  103. package/dist/web/routes/reboot-restore-routes.js.map +1 -0
  104. package/dist/web/routes/session-routes.d.ts +1 -18
  105. package/dist/web/routes/session-routes.d.ts.map +1 -1
  106. package/dist/web/routes/session-routes.js +13 -71
  107. package/dist/web/routes/session-routes.js.map +1 -1
  108. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  109. package/dist/web/routes/ws-routes.js +2 -1
  110. package/dist/web/routes/ws-routes.js.map +1 -1
  111. package/dist/web/schemas.d.ts +12 -0
  112. package/dist/web/schemas.d.ts.map +1 -1
  113. package/dist/web/schemas.js +20 -0
  114. package/dist/web/schemas.js.map +1 -1
  115. package/dist/web/server.d.ts +74 -0
  116. package/dist/web/server.d.ts.map +1 -1
  117. package/dist/web/server.js +243 -2
  118. package/dist/web/server.js.map +1 -1
  119. package/dist/web/session-listener-wiring.d.ts +6 -2
  120. package/dist/web/session-listener-wiring.d.ts.map +1 -1
  121. package/dist/web/session-listener-wiring.js +33 -2
  122. package/dist/web/session-listener-wiring.js.map +1 -1
  123. package/package.json +1 -1
  124. package/dist/web/public/app.556be563.js.br +0 -0
  125. package/dist/web/public/app.556be563.js.gz +0 -0
  126. package/dist/web/public/i18n.561ca09c.js +0 -1
  127. package/dist/web/public/i18n.561ca09c.js.br +0 -0
  128. package/dist/web/public/i18n.561ca09c.js.gz +0 -0
  129. package/dist/web/public/mobile.e9d0b53e.css.br +0 -0
  130. package/dist/web/public/mobile.e9d0b53e.css.gz +0 -0
  131. package/dist/web/public/settings-ui.e0c7f6b4.js +0 -67
  132. package/dist/web/public/settings-ui.e0c7f6b4.js.br +0 -0
  133. package/dist/web/public/settings-ui.e0c7f6b4.js.gz +0 -0
  134. package/dist/web/public/styles.6add175d.css +0 -1
  135. package/dist/web/public/styles.6add175d.css.br +0 -0
  136. package/dist/web/public/styles.6add175d.css.gz +0 -0
  137. package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js +0 -1
  138. package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js.br +0 -0
  139. package/dist/web/public/terminal-keycode229-recovery.6ea8fe37.js.gz +0 -0
  140. package/dist/web/public/terminal-ui.38c49244.js.br +0 -0
  141. package/dist/web/public/terminal-ui.38c49244.js.gz +0 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @fileoverview Decide which sessions a host reboot destroyed and may be rebuilt.
3
+ *
4
+ * A server restart and a host reboot both leave `reconcileSessions()` reporting
5
+ * dead sessions, and they need opposite handling. A server restart leaves the
6
+ * tmux panes running, so recovery ATTACHES to them. A host reboot takes the tmux
7
+ * server down with it, so there is nothing to attach to and the pane has to be
8
+ * created again. This module holds the decision half of that second case, kept
9
+ * free of tmux and disk access so it can be unit tested without either. Every
10
+ * observation it reads is gathered by the caller and passed in.
11
+ *
12
+ * "Eligible" here means a session the user did not end on purpose. The rule that
13
+ * an intentional kill or detach is never auto-revived is enforced at runtime by
14
+ * an in-memory guard in `TmuxManager`, and memory does not survive a reboot. The
15
+ * durable equivalent is the record `cleanupSession()` leaves behind. An unpinned
16
+ * kill deletes the record outright, so it is already absent here. A pinned kill
17
+ * goes through `demoteOrRemoveSession()` and lands as `status: 'stopped'`, which
18
+ * is the marker this module refuses. Pruning keeps a pinned record WITHOUT
19
+ * touching its status, so a pinned session a reboot killed still reads `idle` or
20
+ * `busy` and stays eligible.
21
+ *
22
+ * ⚠️ Ending the AGENT rather than the session is a shape this module CANNOT
23
+ * recognise today, and a reboot restores it. `/exit` ends the CLI inside the
24
+ * pane, `remain-on-exit` keeps the pane, and the PTY Codeman owns is the
25
+ * `tmux attach-session` process, which stays alive throughout — so no exit
26
+ * handler runs, no lifecycle `exit` is logged, and the record keeps both its pid
27
+ * and `status: 'idle'`. Nothing durable distinguishes it from a session that was
28
+ * simply idle when the power went. Ark0N/Codeman#446 covers making Codeman
29
+ * notice the dead pane; until a record can say the agent is gone, this pass will
30
+ * offer those sessions back, and the user dismisses or closes them.
31
+ *
32
+ * The `pid` check below is therefore NOT that rule. It refuses a record whose
33
+ * attach process was already gone, which is a session that never started or
34
+ * whose pane died outright.
35
+ *
36
+ * @dependencies types (SessionState), config/cli-registry
37
+ * @consumedby web/server (plan build at boot), web/routes/reboot-restore-routes
38
+ *
39
+ * @module reboot-restore
40
+ */
41
+ import type { SessionState } from './types.js';
42
+ /** Observations the reboot heuristic reads. Gathered by the caller, never here. */
43
+ export interface RebootEvidence {
44
+ /** Sessions that still had a live pane during reconciliation. */
45
+ livePaneCount: number;
46
+ /** Sessions reconciliation just marked dead. */
47
+ deadSessionCount: number;
48
+ /** `os.uptime()`, in seconds. */
49
+ uptimeSeconds: number;
50
+ /** Newest `lastActivityAt` across the persisted records, in ms since the epoch. */
51
+ newestPersistedActivityAt: number;
52
+ /** `Date.now()` when the evidence was gathered, in ms. */
53
+ now: number;
54
+ }
55
+ /**
56
+ * Decide whether the machine plausibly rebooted rather than the server restarting.
57
+ *
58
+ * Two signals have to agree. The socket must hold no panes at all while state
59
+ * still lists sessions, which rules out an ordinary server restart. The host
60
+ * must also have booted after the newest persisted session activity, which is
61
+ * the corroboration `os.uptime()` provides cheaply. A wiped tmux socket on a
62
+ * long-uptime host fails the second test, so a user who killed the tmux server
63
+ * by hand does not get every session offered back to them.
64
+ *
65
+ * This heuristic decides whether to ASK, never whether to act. A wrong yes costs
66
+ * the user a banner they dismiss, because the restore itself waits for a click.
67
+ *
68
+ * ⚠️ `os.uptime()` reports the HOST's uptime, which a container shares, and that
69
+ * cuts BOTH ways rather than simply switching the feature off in Docker. After a
70
+ * genuine host reboot a containerized Codeman sees the host's short uptime, so the
71
+ * banner DOES appear and the feature works. What it cannot see is a container-only
72
+ * restart: the host uptime is long, the boot test fails, and no banner appears
73
+ * although every in-container pane is gone (`docker/server.Dockerfile` installs
74
+ * tmux inside the Codeman container, and the self-updater restarts the Compose
75
+ * deployment by exiting the container, so that is the case where this would help
76
+ * most). Failing quiet is the safe direction, and closing the gap needs a boot
77
+ * signal the container owns (PID 1's start time, gated on the existing
78
+ * `isRunningInContainer()`) rather than a wider heuristic.
79
+ */
80
+ export declare function looksLikeHostReboot(evidence: RebootEvidence): boolean;
81
+ /**
82
+ * Pick the conversation the rebuilt pane should resume.
83
+ *
84
+ * The chain's tail is the newest conversation the session was holding, which is
85
+ * what a compact or a clear leaves behind; `resumeSessionId` covers a session
86
+ * that was itself started as a resume, and the session id is the original
87
+ * conversation for everything else.
88
+ */
89
+ export declare function resolveResumeConversationId(state: SessionState): string;
90
+ /**
91
+ * Why one session was passed over. Reported for logging and shown to the user.
92
+ *
93
+ * The first seven are decided before anything is built. `capacity-reached` and
94
+ * `rebuild-failed` can only happen once a click is spending the plan, and they
95
+ * are the two the banner must not confuse with a missing workspace: one means
96
+ * "try again after closing something", the other means the CLI would not start.
97
+ */
98
+ export interface RebootRestoreRejection {
99
+ sessionId: string;
100
+ reason: 'no-persisted-record' | 'intentionally-ended' | 'not-running' | 'respawn-blocked' | 'remote-or-docker' | 'unsupported-mode' | 'no-working-dir' | 'workspace-missing' | 'workspace-forbidden' | 'already-live' | 'capacity-reached' | 'rebuild-failed';
101
+ }
102
+ /** One restorable session, as the banner shows it and the rebuild replays it. */
103
+ export interface RebootRestoreEntry {
104
+ sessionId: string;
105
+ name?: string;
106
+ workingDir: string;
107
+ owner?: string;
108
+ mode: string;
109
+ /** The conversation the rebuilt pane resumes. */
110
+ resumeConversationId: string;
111
+ /**
112
+ * The persisted record, kept whole so the rebuild can replay what it held.
113
+ * Read at boot, before pruning deletes it, and held in memory until the click.
114
+ */
115
+ state: SessionState;
116
+ }
117
+ export interface RebootRestorePlan {
118
+ restore: RebootRestoreEntry[];
119
+ skipped: RebootRestoreRejection[];
120
+ }
121
+ /**
122
+ * Split the sessions reconciliation just killed into the ones a reboot restore
123
+ * may offer and the ones it must leave alone.
124
+ *
125
+ * @param deadSessionIds Session ids `reconcileSessions()` reported as dead.
126
+ * @param persisted The `state.json` session records, which `cleanupStaleSessions()`
127
+ * has not pruned yet at the point this runs.
128
+ * @param workspaceExists Whether a working directory is still on disk. A tmux
129
+ * session can outlive its deleted repo, and rebuilding one there would scaffold
130
+ * an empty tree. The caller owns the disk access; the click re-checks, because
131
+ * a repo can be deleted between the boot and the click.
132
+ */
133
+ export declare function planRebootRestore(deadSessionIds: readonly string[], persisted: Readonly<Record<string, SessionState>>, workspaceExists: (workingDir: string) => boolean): RebootRestorePlan;
134
+ /**
135
+ * Drop the entries whose conversation is already on screen.
136
+ *
137
+ * Hours can pass between the boot that built the plan and the click that spends
138
+ * it, and the Resume list can reach the same conversation in the meantime. Two
139
+ * panes running `claude --resume` on one conversation is the failure this
140
+ * prevents, so a match on either the session id or the conversation id is enough
141
+ * to skip the entry.
142
+ */
143
+ export declare function rejectAlreadyLive(entries: readonly RebootRestoreEntry[], liveSessionIds: ReadonlySet<string>, liveConversationIds: ReadonlySet<string>): RebootRestorePlan;
144
+ /** Newest `lastActivityAt` across persisted records, or 0 when there are none. */
145
+ export declare function newestPersistedActivity(persisted: Readonly<Record<string, SessionState>>): number;
146
+ //# sourceMappingURL=reboot-restore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reboot-restore.d.ts","sourceRoot":"","sources":["../src/reboot-restore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,gBAAgB,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,yBAAyB,EAAE,MAAM,CAAC;IAClC,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAMrE;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAIvE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EACF,qBAAqB,GACrB,qBAAqB,GACrB,aAAa,GACb,iBAAiB,GACjB,kBAAkB,GAClB,kBAAkB,GAClB,gBAAgB,GAChB,mBAAmB,GACnB,qBAAqB,GACrB,cAAc,GACd,kBAAkB,GAClB,gBAAgB,CAAC;CACtB;AAED,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,cAAc,EAAE,SAAS,MAAM,EAAE,EACjC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,EACjD,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,GAC/C,iBAAiB,CAuEnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,kBAAkB,EAAE,EACtC,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,EACnC,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,GACvC,iBAAiB,CAWnB;AAED,kFAAkF;AAClF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,GAAG,MAAM,CAOjG"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @fileoverview Decide which sessions a host reboot destroyed and may be rebuilt.
3
+ *
4
+ * A server restart and a host reboot both leave `reconcileSessions()` reporting
5
+ * dead sessions, and they need opposite handling. A server restart leaves the
6
+ * tmux panes running, so recovery ATTACHES to them. A host reboot takes the tmux
7
+ * server down with it, so there is nothing to attach to and the pane has to be
8
+ * created again. This module holds the decision half of that second case, kept
9
+ * free of tmux and disk access so it can be unit tested without either. Every
10
+ * observation it reads is gathered by the caller and passed in.
11
+ *
12
+ * "Eligible" here means a session the user did not end on purpose. The rule that
13
+ * an intentional kill or detach is never auto-revived is enforced at runtime by
14
+ * an in-memory guard in `TmuxManager`, and memory does not survive a reboot. The
15
+ * durable equivalent is the record `cleanupSession()` leaves behind. An unpinned
16
+ * kill deletes the record outright, so it is already absent here. A pinned kill
17
+ * goes through `demoteOrRemoveSession()` and lands as `status: 'stopped'`, which
18
+ * is the marker this module refuses. Pruning keeps a pinned record WITHOUT
19
+ * touching its status, so a pinned session a reboot killed still reads `idle` or
20
+ * `busy` and stays eligible.
21
+ *
22
+ * ⚠️ Ending the AGENT rather than the session is a shape this module CANNOT
23
+ * recognise today, and a reboot restores it. `/exit` ends the CLI inside the
24
+ * pane, `remain-on-exit` keeps the pane, and the PTY Codeman owns is the
25
+ * `tmux attach-session` process, which stays alive throughout — so no exit
26
+ * handler runs, no lifecycle `exit` is logged, and the record keeps both its pid
27
+ * and `status: 'idle'`. Nothing durable distinguishes it from a session that was
28
+ * simply idle when the power went. Ark0N/Codeman#446 covers making Codeman
29
+ * notice the dead pane; until a record can say the agent is gone, this pass will
30
+ * offer those sessions back, and the user dismisses or closes them.
31
+ *
32
+ * The `pid` check below is therefore NOT that rule. It refuses a record whose
33
+ * attach process was already gone, which is a session that never started or
34
+ * whose pane died outright.
35
+ *
36
+ * @dependencies types (SessionState), config/cli-registry
37
+ * @consumedby web/server (plan build at boot), web/routes/reboot-restore-routes
38
+ *
39
+ * @module reboot-restore
40
+ */
41
+ import { getCli } from './config/cli-registry/registry.js';
42
+ /** Session statuses a reboot restore may rebuild. `stopped` is the kill marker. */
43
+ const RESTORABLE_STATUSES = new Set(['idle', 'busy', 'error']);
44
+ /**
45
+ * Decide whether the machine plausibly rebooted rather than the server restarting.
46
+ *
47
+ * Two signals have to agree. The socket must hold no panes at all while state
48
+ * still lists sessions, which rules out an ordinary server restart. The host
49
+ * must also have booted after the newest persisted session activity, which is
50
+ * the corroboration `os.uptime()` provides cheaply. A wiped tmux socket on a
51
+ * long-uptime host fails the second test, so a user who killed the tmux server
52
+ * by hand does not get every session offered back to them.
53
+ *
54
+ * This heuristic decides whether to ASK, never whether to act. A wrong yes costs
55
+ * the user a banner they dismiss, because the restore itself waits for a click.
56
+ *
57
+ * ⚠️ `os.uptime()` reports the HOST's uptime, which a container shares, and that
58
+ * cuts BOTH ways rather than simply switching the feature off in Docker. After a
59
+ * genuine host reboot a containerized Codeman sees the host's short uptime, so the
60
+ * banner DOES appear and the feature works. What it cannot see is a container-only
61
+ * restart: the host uptime is long, the boot test fails, and no banner appears
62
+ * although every in-container pane is gone (`docker/server.Dockerfile` installs
63
+ * tmux inside the Codeman container, and the self-updater restarts the Compose
64
+ * deployment by exiting the container, so that is the case where this would help
65
+ * most). Failing quiet is the safe direction, and closing the gap needs a boot
66
+ * signal the container owns (PID 1's start time, gated on the existing
67
+ * `isRunningInContainer()`) rather than a wider heuristic.
68
+ */
69
+ export function looksLikeHostReboot(evidence) {
70
+ if (evidence.deadSessionCount === 0)
71
+ return false;
72
+ if (evidence.livePaneCount > 0)
73
+ return false;
74
+ if (evidence.newestPersistedActivityAt <= 0)
75
+ return false;
76
+ const bootedAt = evidence.now - evidence.uptimeSeconds * 1000;
77
+ return bootedAt > evidence.newestPersistedActivityAt;
78
+ }
79
+ /**
80
+ * Pick the conversation the rebuilt pane should resume.
81
+ *
82
+ * The chain's tail is the newest conversation the session was holding, which is
83
+ * what a compact or a clear leaves behind; `resumeSessionId` covers a session
84
+ * that was itself started as a resume, and the session id is the original
85
+ * conversation for everything else.
86
+ */
87
+ export function resolveResumeConversationId(state) {
88
+ const chain = state.claudeSessionChain;
89
+ const chainTail = Array.isArray(chain) && chain.length > 0 ? chain[chain.length - 1] : undefined;
90
+ return chainTail || state.resumeSessionId || state.id;
91
+ }
92
+ /**
93
+ * Split the sessions reconciliation just killed into the ones a reboot restore
94
+ * may offer and the ones it must leave alone.
95
+ *
96
+ * @param deadSessionIds Session ids `reconcileSessions()` reported as dead.
97
+ * @param persisted The `state.json` session records, which `cleanupStaleSessions()`
98
+ * has not pruned yet at the point this runs.
99
+ * @param workspaceExists Whether a working directory is still on disk. A tmux
100
+ * session can outlive its deleted repo, and rebuilding one there would scaffold
101
+ * an empty tree. The caller owns the disk access; the click re-checks, because
102
+ * a repo can be deleted between the boot and the click.
103
+ */
104
+ export function planRebootRestore(deadSessionIds, persisted, workspaceExists) {
105
+ const restore = [];
106
+ const skipped = [];
107
+ for (const sessionId of deadSessionIds) {
108
+ const state = persisted[sessionId];
109
+ if (!state) {
110
+ // An unpinned kill already deleted the record, so absence IS the guard.
111
+ skipped.push({ sessionId, reason: 'no-persisted-record' });
112
+ continue;
113
+ }
114
+ if (!RESTORABLE_STATUSES.has(state.status)) {
115
+ // A pinned kill was demoted to `stopped`. Reviving it would undo the kill.
116
+ skipped.push({ sessionId, reason: 'intentionally-ended' });
117
+ continue;
118
+ }
119
+ if (state.pid === null || state.pid === undefined) {
120
+ // No attach process when the record was last written: the session never
121
+ // started, or its pane died outright rather than its agent exiting inside a
122
+ // surviving pane. Either way there was nothing running to bring back.
123
+ //
124
+ // ⚠️ This does NOT catch a session the user ended with `/exit`. See the
125
+ // module header: that leaves the pid in place, because the pid is the tmux
126
+ // attach process and `remain-on-exit` keeps it alive.
127
+ //
128
+ // Conservative on purpose. A session that somehow persisted no pid while
129
+ // genuinely running is not offered, and its conversation stays reachable
130
+ // from the Resume list, which is where every session would be without this
131
+ // feature.
132
+ skipped.push({ sessionId, reason: 'not-running' });
133
+ continue;
134
+ }
135
+ if (state.respawnBlocked === true) {
136
+ // The crash-loop breaker tripped on this pane. Re-creating it restarts the loop.
137
+ skipped.push({ sessionId, reason: 'respawn-blocked' });
138
+ continue;
139
+ }
140
+ if (state.remote || state.docker) {
141
+ // Both need another host or a container to be up, which a just-booted machine
142
+ // cannot promise. The remote reconnect watcher owns the remote case already.
143
+ skipped.push({ sessionId, reason: 'remote-or-docker' });
144
+ continue;
145
+ }
146
+ // Capability, not a CLI id: this pass resumes by handing the CLI a conversation
147
+ // id through the top-level `resumeSessionId`, which only a CLI whose history the
148
+ // claude-jsonl reader understands can consume that way. Others carry their thread
149
+ // id in their own `<Mode>Config`, which this pass does not thread through.
150
+ if (getCli(state.mode ?? 'claude')?.capabilities.transcript !== 'claude-jsonl') {
151
+ skipped.push({ sessionId, reason: 'unsupported-mode' });
152
+ continue;
153
+ }
154
+ if (!state.workingDir) {
155
+ skipped.push({ sessionId, reason: 'no-working-dir' });
156
+ continue;
157
+ }
158
+ if (!workspaceExists(state.workingDir)) {
159
+ skipped.push({ sessionId, reason: 'workspace-missing' });
160
+ continue;
161
+ }
162
+ restore.push({
163
+ sessionId,
164
+ name: state.name,
165
+ workingDir: state.workingDir,
166
+ owner: state.owner,
167
+ mode: state.mode ?? 'claude',
168
+ resumeConversationId: resolveResumeConversationId(state),
169
+ state,
170
+ });
171
+ }
172
+ return { restore, skipped };
173
+ }
174
+ /**
175
+ * Drop the entries whose conversation is already on screen.
176
+ *
177
+ * Hours can pass between the boot that built the plan and the click that spends
178
+ * it, and the Resume list can reach the same conversation in the meantime. Two
179
+ * panes running `claude --resume` on one conversation is the failure this
180
+ * prevents, so a match on either the session id or the conversation id is enough
181
+ * to skip the entry.
182
+ */
183
+ export function rejectAlreadyLive(entries, liveSessionIds, liveConversationIds) {
184
+ const restore = [];
185
+ const skipped = [];
186
+ for (const entry of entries) {
187
+ if (liveSessionIds.has(entry.sessionId) || liveConversationIds.has(entry.resumeConversationId)) {
188
+ skipped.push({ sessionId: entry.sessionId, reason: 'already-live' });
189
+ continue;
190
+ }
191
+ restore.push(entry);
192
+ }
193
+ return { restore, skipped };
194
+ }
195
+ /** Newest `lastActivityAt` across persisted records, or 0 when there are none. */
196
+ export function newestPersistedActivity(persisted) {
197
+ let newest = 0;
198
+ for (const state of Object.values(persisted)) {
199
+ const stamp = state.lastActivityAt ?? state.createdAt ?? 0;
200
+ if (stamp > newest)
201
+ newest = stamp;
202
+ }
203
+ return newest;
204
+ }
205
+ //# sourceMappingURL=reboot-restore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reboot-restore.js","sourceRoot":"","sources":["../src/reboot-restore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @fileoverview Automatic session names from the first prompt.
3
+ *
4
+ * A new tab is born as `w3-myapp`, which says where it runs and nothing about
5
+ * what it is doing. Once the user submits a real prompt the tab can carry a
6
+ * title derived from it (`w3-myapp: fix the login redirect`), and this module
7
+ * holds the three pure pieces of that: a tracker that reconstructs the composer
8
+ * text from the keystrokes Codeman forwards, the title heuristic, and the
9
+ * prefix-preserving composition.
10
+ *
11
+ * Deliberately no LLM: the prompt already passes through the input boundary,
12
+ * so a local title is private, deterministic and identical for every CLI.
13
+ *
14
+ * ⚠️ The tracker sits on the raw keystroke stream, which carries far more than
15
+ * the prompt: cursor keys, mouse reports Codeman forwards to the CLI, bracketed
16
+ * pastes, Alt chords, the bare Esc that interrupts a turn. Every one of those
17
+ * once named a tab something wrong (a lone Esc ate the next prompt's first
18
+ * character; a wheel tick mid-word dropped the first half of the prompt), so
19
+ * the rules below are explicit per key. The model is a best-effort transcript:
20
+ * keys whose effect on the composer is knowable are mirrored, keys that leave
21
+ * the text alone are ignored, and keys that replace it with something the
22
+ * tracker cannot see (history recall) TAINT the draft so that Enter submits
23
+ * nothing rather than a fragment. A prompt that yields no title leaves the
24
+ * session eligible for the next one.
25
+ *
26
+ * Only user-originated input is fed here; the Session decides that. Ralph
27
+ * kick-starts, respawn `/clear`s, cron launches and approval answers all go
28
+ * through the same write paths and must never become a tab title.
29
+ *
30
+ * @module session-auto-name
31
+ */
32
+ /**
33
+ * Reconstructs the composer draft from forwarded keystrokes and reports each
34
+ * submitted prompt. Input arrives in arbitrary chunks (one keystroke, a paste,
35
+ * an agent's whole prompt plus Enter), so all state lives across calls.
36
+ */
37
+ export declare class SubmittedPromptTracker {
38
+ private buffer;
39
+ private bufferCodePoints;
40
+ /** Code points typed past the cap; backspaces eat these before real text. */
41
+ private overflow;
42
+ /** Escape sequence in progress; a lone ESC means "just saw ESC". */
43
+ private sequence;
44
+ private inPaste;
45
+ /** The composer holds text the tracker never saw (history recall); Enter submits nothing. */
46
+ private tainted;
47
+ feed(data: string): string[];
48
+ private continueSequence;
49
+ private handleKeyOrEscape;
50
+ private handleCsi;
51
+ private handleKey;
52
+ /** One space between lines, never a run of them, and none at the start. */
53
+ private appendSeparator;
54
+ private append;
55
+ private backspace;
56
+ private killWord;
57
+ private reset;
58
+ }
59
+ /**
60
+ * Turns a submitted prompt into a title, or null when the prompt is not a task:
61
+ * empty, a slash command (`/clear`, `/model`), or a `!` shell escape.
62
+ */
63
+ export declare function deriveAutoSessionName(prompt: string): string | null;
64
+ /**
65
+ * The name a placeholder becomes: `<prefix>: <title>`, so the tab keeps its
66
+ * case identity and its `w<n>` counter (the tab strip already renders that
67
+ * form as the title alone, prefix in the tooltip, and the next-session counter
68
+ * still matches it). A session with no name at all just takes the title. The
69
+ * result honours `maxLength` in UTF-16 units, the unit the rename route caps.
70
+ */
71
+ export declare function composeAutoSessionName(currentName: string, title: string, maxLength?: number): string;
72
+ /** Codeman's own `w<n>-<case>` / `s<n>-<case>` placeholders, the only names auto-naming replaces. */
73
+ export declare function isGeneratedSessionName(name: string): boolean;
74
+ //# sourceMappingURL=session-auto-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-auto-name.d.ts","sourceRoot":"","sources":["../src/session-auto-name.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AA4CH;;;;GAIG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAK;IACrB,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,6FAA6F;IAC7F,OAAO,CAAC,OAAO,CAAS;IAExB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAsB5B,OAAO,CAAC,gBAAgB;IA8CxB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,SAAS;IAiBjB,OAAO,CAAC,SAAS;IA8CjB,2EAA2E;IAC3E,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,MAAM;IASd,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,KAAK;CAMd;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAInE;AA8BD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,SAA0B,GAClC,MAAM,CAMR;AAeD,qGAAqG;AACrG,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D"}