@raidiant/notifai 10.1.0 โ†’ 10.1.2

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 (204) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -0
  3. package/dist/cli-bin.d.ts +21 -1
  4. package/dist/cli-bin.js +141 -10
  5. package/dist/cli-bin.js.map +1 -1
  6. package/dist/cli-update-contract.d.ts +7 -0
  7. package/dist/cli-update-contract.js +10 -0
  8. package/dist/cli-update-contract.js.map +1 -0
  9. package/dist/client.d.ts +6 -1
  10. package/dist/client.js +1 -0
  11. package/dist/client.js.map +1 -1
  12. package/dist/codex-wake.d.ts +2 -0
  13. package/dist/codex-wake.js +2 -1
  14. package/dist/codex-wake.js.map +1 -1
  15. package/dist/commands-auth.d.ts +15 -1
  16. package/dist/commands-auth.js +29 -11
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-core.d.ts +9 -2
  19. package/dist/commands-core.js +11 -4
  20. package/dist/commands-core.js.map +1 -1
  21. package/dist/commands-devices.d.ts +10 -1
  22. package/dist/commands-devices.js +17 -13
  23. package/dist/commands-devices.js.map +1 -1
  24. package/dist/commands-doctor.js +93 -49
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-hooks.js +47 -10
  27. package/dist/commands-hooks.js.map +1 -1
  28. package/dist/commands-init.d.ts +16 -0
  29. package/dist/commands-init.js +254 -93
  30. package/dist/commands-init.js.map +1 -1
  31. package/dist/commands-send-support.js +3 -0
  32. package/dist/commands-send-support.js.map +1 -1
  33. package/dist/commands-send.d.ts +1 -1
  34. package/dist/commands-send.js +121 -35
  35. package/dist/commands-send.js.map +1 -1
  36. package/dist/commands-skill.js +72 -54
  37. package/dist/commands-skill.js.map +1 -1
  38. package/dist/commands-update.d.ts +10 -0
  39. package/dist/commands-update.js +141 -0
  40. package/dist/commands-update.js.map +1 -0
  41. package/dist/commands.d.ts +2 -1
  42. package/dist/commands.js +2 -1
  43. package/dist/commands.js.map +1 -1
  44. package/dist/hook-adapter.d.ts +2 -0
  45. package/dist/hook-adapter.js +12 -1
  46. package/dist/hook-adapter.js.map +1 -1
  47. package/dist/hooks.d.ts +43 -2
  48. package/dist/hooks.js +163 -21
  49. package/dist/hooks.js.map +1 -1
  50. package/dist/invocation-context.d.ts +1 -0
  51. package/dist/invocation-context.js +9 -1
  52. package/dist/invocation-context.js.map +1 -1
  53. package/dist/main-run.d.ts +1 -0
  54. package/dist/main-run.js +42 -0
  55. package/dist/main-run.js.map +1 -0
  56. package/dist/main.js +10 -31
  57. package/dist/main.js.map +1 -1
  58. package/dist/native-skills.d.ts +13 -3
  59. package/dist/native-skills.js +51 -12
  60. package/dist/native-skills.js.map +1 -1
  61. package/dist/node-floor.d.ts +19 -0
  62. package/dist/node-floor.js +39 -0
  63. package/dist/node-floor.js.map +1 -0
  64. package/dist/orca-session-title.d.ts +5 -4
  65. package/dist/orca-session-title.js +32 -19
  66. package/dist/orca-session-title.js.map +1 -1
  67. package/dist/platform.d.ts +11 -17
  68. package/dist/platform.js +20 -23
  69. package/dist/platform.js.map +1 -1
  70. package/dist/program.d.ts +2 -1
  71. package/dist/program.js +22 -5
  72. package/dist/program.js.map +1 -1
  73. package/dist/question-settlement-process.d.ts +15 -0
  74. package/dist/question-settlement-process.js +35 -0
  75. package/dist/question-settlement-process.js.map +1 -0
  76. package/dist/readiness.d.ts +25 -1
  77. package/dist/readiness.js +32 -2
  78. package/dist/readiness.js.map +1 -1
  79. package/dist/release.d.ts +5 -5
  80. package/dist/release.js +5 -5
  81. package/dist/setup-destinations.d.ts +23 -0
  82. package/dist/setup-destinations.js +44 -0
  83. package/dist/setup-destinations.js.map +1 -0
  84. package/dist/skill-integrity.d.ts +49 -0
  85. package/dist/skill-integrity.js +154 -0
  86. package/dist/skill-integrity.js.map +1 -0
  87. package/dist/skill-source/manifest.json +16 -0
  88. package/dist/skill-source/notifai/SKILL.md +390 -0
  89. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  90. package/package.json +11 -14
  91. package/src/atomic-file.ts +0 -198
  92. package/src/claude-wake.test.ts +0 -321
  93. package/src/claude-wake.ts +0 -499
  94. package/src/cli-bin.test.ts +0 -38
  95. package/src/cli-bin.ts +0 -67
  96. package/src/cli-release.test.ts +0 -46
  97. package/src/cli-release.ts +0 -78
  98. package/src/client.test.ts +0 -390
  99. package/src/client.ts +0 -345
  100. package/src/codex-wake.test.ts +0 -254
  101. package/src/codex-wake.ts +0 -297
  102. package/src/command-session.ts +0 -38
  103. package/src/commands-agent-sessions.ts +0 -72
  104. package/src/commands-auth.ts +0 -205
  105. package/src/commands-config.ts +0 -336
  106. package/src/commands-core.ts +0 -298
  107. package/src/commands-devices.test.ts +0 -15
  108. package/src/commands-devices.ts +0 -173
  109. package/src/commands-doctor.ts +0 -1440
  110. package/src/commands-guidance.ts +0 -181
  111. package/src/commands-harness-context.ts +0 -207
  112. package/src/commands-hook-shape.ts +0 -69
  113. package/src/commands-hooks.ts +0 -2268
  114. package/src/commands-init.ts +0 -878
  115. package/src/commands-io.ts +0 -108
  116. package/src/commands-logs.ts +0 -243
  117. package/src/commands-project.ts +0 -47
  118. package/src/commands-send-support.test.ts +0 -156
  119. package/src/commands-send-support.ts +0 -311
  120. package/src/commands-send.ts +0 -769
  121. package/src/commands-setup-proof.test.ts +0 -72
  122. package/src/commands-setup-proof.ts +0 -124
  123. package/src/commands-skill.test.ts +0 -44
  124. package/src/commands-skill.ts +0 -204
  125. package/src/commands-sounds.test.ts +0 -106
  126. package/src/commands-sounds.ts +0 -49
  127. package/src/commands.test.ts +0 -7828
  128. package/src/commands.ts +0 -75
  129. package/src/config-schema.test.ts +0 -153
  130. package/src/config-schema.ts +0 -349
  131. package/src/config.test.ts +0 -581
  132. package/src/config.ts +0 -475
  133. package/src/credentials.test.ts +0 -403
  134. package/src/credentials.ts +0 -403
  135. package/src/file-lock-process-worker.test.ts +0 -64
  136. package/src/file-lock.test.ts +0 -320
  137. package/src/file-lock.ts +0 -393
  138. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  139. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  140. package/src/generated-session-label.ts +0 -155
  141. package/src/guidance-content.ts +0 -278
  142. package/src/guidance-render.ts +0 -56
  143. package/src/guidance.test.ts +0 -327
  144. package/src/guidance.ts +0 -233
  145. package/src/harness-integration.test.ts +0 -294
  146. package/src/harnesses.test.ts +0 -149
  147. package/src/harnesses.ts +0 -164
  148. package/src/hook-adapter.test.ts +0 -496
  149. package/src/hook-adapter.ts +0 -410
  150. package/src/hook-input.test.ts +0 -39
  151. package/src/hook-input.ts +0 -72
  152. package/src/hooks.test.ts +0 -5549
  153. package/src/hooks.ts +0 -3759
  154. package/src/install-hooks.test.ts +0 -1614
  155. package/src/install-hooks.ts +0 -1501
  156. package/src/interactive.test.ts +0 -387
  157. package/src/interactive.ts +0 -858
  158. package/src/invocation-context.test.ts +0 -352
  159. package/src/invocation-context.ts +0 -197
  160. package/src/logging-process-worker.test.ts +0 -45
  161. package/src/logging.test.ts +0 -544
  162. package/src/logging.ts +0 -698
  163. package/src/main.ts +0 -36
  164. package/src/native-skills.test.ts +0 -115
  165. package/src/native-skills.ts +0 -162
  166. package/src/onboarding-docs.test.ts +0 -25
  167. package/src/openclaw-plugin.ts +0 -409
  168. package/src/opencode-plugin.ts +0 -298
  169. package/src/orca-session-title.test.ts +0 -54
  170. package/src/orca-session-title.ts +0 -103
  171. package/src/pack-feedback-slice.test.ts +0 -212
  172. package/src/pack-feedback-slice.ts +0 -187
  173. package/src/platform.test.ts +0 -152
  174. package/src/platform.ts +0 -173
  175. package/src/program.test.ts +0 -282
  176. package/src/program.ts +0 -818
  177. package/src/project-enablement.test.ts +0 -55
  178. package/src/project-enablement.ts +0 -99
  179. package/src/question-caps.test.ts +0 -49
  180. package/src/question-timing.ts +0 -56
  181. package/src/readiness.test.ts +0 -25
  182. package/src/readiness.ts +0 -179
  183. package/src/release.ts +0 -51
  184. package/src/send-attempts.test.ts +0 -143
  185. package/src/send-attempts.ts +0 -188
  186. package/src/send.test.ts +0 -28
  187. package/src/send.ts +0 -436
  188. package/src/session-activation.ts +0 -61
  189. package/src/session-end-log-lock-worker.test.ts +0 -35
  190. package/src/session-labels.test.ts +0 -490
  191. package/src/session-labels.ts +0 -474
  192. package/src/skill.test.ts +0 -415
  193. package/src/sound-ref.test.ts +0 -31
  194. package/src/sound-ref.ts +0 -34
  195. package/src/trust-policy.test.ts +0 -701
  196. package/src/ui/banner.ts +0 -95
  197. package/src/ui/config-view.test.ts +0 -90
  198. package/src/ui/config-view.ts +0 -138
  199. package/src/ui/help.test.ts +0 -16
  200. package/src/ui/help.ts +0 -128
  201. package/src/ui/theme.test.ts +0 -92
  202. package/src/ui/theme.ts +0 -265
  203. package/src/url-policy.ts +0 -399
  204. package/tsconfig.json +0 -10
@@ -1,61 +0,0 @@
1
- import type { HookHarness } from './hooks.js'
2
- import { Buffer } from 'node:buffer'
3
- import { GUIDANCE_CONTEXT_MAX_BYTES, boundedEffectiveGuidance } from './guidance-render.js'
4
-
5
- const ROOT_OWNERSHIP =
6
- 'Notifai is enabled for this Project. The effective, provenance-marked guidance below governs Notification Requests. ' +
7
- 'Missing CLI readiness is handled through `notifai init --json`.'
8
-
9
- export const WORKER_ACTIVATION_CONTEXT =
10
- 'Notifai worker context: an Agent Event is a meaningful occurrence in your work; a Notification Request is a deliberate User-visible message about one. Report Agent Events to the parent and do not send Notification Requests unless the parent explicitly delegated that ownership. If delegated, load the Notifai skill and run `notifai guidance` before composing one.'
11
-
12
- export const MISSING_LIFECYCLE_GUIDANCE_CONTEXT =
13
- 'Notifai is active, but lifecycle guidance could not be loaded. Run `notifai guidance` once before deciding whether or how to send a Notification Request.'
14
-
15
- function rootActivationContext(cwd: string, env: NodeJS.ProcessEnv): string {
16
- const guidance = boundedEffectiveGuidance({
17
- cwd,
18
- env,
19
- maxBytes: GUIDANCE_CONTEXT_MAX_BYTES - Buffer.byteLength(`${ROOT_OWNERSHIP}\n\n`, 'utf8'),
20
- })
21
- return `${ROOT_OWNERSHIP}\n\n${guidance.ok ? guidance.content : guidance.fallback}`
22
- }
23
-
24
- /** One lifecycle meaning, encoded for each harness output contract. */
25
- export function sessionActivationOutput(
26
- harness: HookHarness | undefined,
27
- hookEventName: 'SessionStart' | 'SubagentStart',
28
- cwd: string = process.cwd(),
29
- env: NodeJS.ProcessEnv = process.env,
30
- ): string | undefined {
31
- const context = hookEventName === 'SubagentStart'
32
- ? WORKER_ACTIVATION_CONTEXT
33
- : rootActivationContext(cwd, env)
34
- if (harness === 'opencode' || harness === 'openclaw') return context
35
- if (harness === 'cursor') {
36
- return JSON.stringify({ additional_context: context })
37
- }
38
- if (harness === 'claude-code' || harness === 'codex') {
39
- return JSON.stringify({
40
- hookSpecificOutput: {
41
- hookEventName,
42
- additionalContext: context,
43
- },
44
- })
45
- }
46
- return undefined
47
- }
48
-
49
- /**
50
- * Cursor currently accepts sessionStart context without reliably delivering it
51
- * to the model. Its native post-Stop follow-up is the narrow fallback and is
52
- * claimed once per conversation by the hook-state layer.
53
- */
54
- export function cursorStopActivationOutput(
55
- cwd: string = process.cwd(),
56
- env: NodeJS.ProcessEnv = process.env,
57
- ): string {
58
- return JSON.stringify({
59
- followup_message: rootActivationContext(cwd, env),
60
- })
61
- }
@@ -1,35 +0,0 @@
1
- import { existsSync, writeFileSync } from 'node:fs'
2
- import { describe, expect, it } from 'vitest'
3
- import { withFileLock } from './file-lock.js'
4
-
5
- const workerMode = process.env['NOTIFAI_SESSION_END_LOG_LOCK_WORKER_MODE']
6
- const sleep = new Int32Array(new SharedArrayBuffer(4))
7
-
8
- function waitFor(file: string): void {
9
- const deadline = Date.now() + 15_000
10
- while (!existsSync(file)) {
11
- if (Date.now() >= deadline) throw new Error(`timed out waiting for ${file}`)
12
- Atomics.wait(sleep, 0, 0, 2)
13
- }
14
- }
15
-
16
- if (workerMode === undefined) {
17
- describe('SessionEnd log lock worker', () => {
18
- it('stays dormant outside the CLI deadline regression', () => {
19
- expect(workerMode).toBeUndefined()
20
- })
21
- })
22
- } else {
23
- describe('SessionEnd log lock worker', () => {
24
- it('holds a live bakery ticket until the parent releases it', () => {
25
- withFileLock(
26
- process.env['NOTIFAI_SESSION_END_LOG_LOCK_PATH']!,
27
- () => {
28
- writeFileSync(process.env['NOTIFAI_SESSION_END_LOG_LOCK_READY']!, workerMode)
29
- waitFor(process.env['NOTIFAI_SESSION_END_LOG_LOCK_RELEASE']!)
30
- },
31
- { waitMs: 15_000 },
32
- )
33
- })
34
- })
35
- }
@@ -1,490 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- mkdtempSync,
5
- readFileSync,
6
- readdirSync,
7
- writeFileSync,
8
- } from 'node:fs'
9
- import os from 'node:os'
10
- import path from 'node:path'
11
- import { describe, expect, it } from 'vitest'
12
- import { stateDir } from './config.js'
13
- import {
14
- formatSessionFirstSeen,
15
- renameStoredSessionLabel,
16
- resolveSessionLabel,
17
- } from './session-labels.js'
18
-
19
- function fixture(): { env: NodeJS.ProcessEnv; now: number } {
20
- const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-session-labels-'))
21
- return {
22
- env: { XDG_STATE_HOME: root },
23
- now: new Date(2026, 7, 20, 14, 5).getTime(),
24
- }
25
- }
26
-
27
- describe('semantic session labels', () => {
28
- it('freezes the first explicit task label for the immutable session', () => {
29
- const { env, now } = fixture()
30
- expect(
31
- resolveSessionLabel({
32
- env,
33
- now,
34
- sessionId: 'opaque-session-one',
35
- harness: 'claude-code',
36
- explicitLabel: ' Semantic session names ',
37
- }),
38
- ).toEqual({ ok: true, label: 'Semantic session names', source: 'explicit' })
39
-
40
- expect(
41
- resolveSessionLabel({
42
- env,
43
- now: now + 1_000,
44
- sessionId: 'opaque-session-one',
45
- harness: 'claude-code',
46
- explicitLabel: 'A later unrelated label',
47
- }),
48
- ).toEqual({ ok: true, label: 'Semantic session names', source: 'explicit' })
49
- expect(
50
- resolveSessionLabel({
51
- env,
52
- now: now + 2_000,
53
- sessionId: 'opaque-session-one',
54
- harness: 'claude-code',
55
- explicitLabel: 'x'.repeat(100),
56
- }),
57
- ).toEqual({ ok: true, label: 'Semantic session names', source: 'explicit' })
58
- })
59
-
60
- it('prefers the environment-supplied title over an explicit label, making the flag safe everywhere', () => {
61
- // An agent must never need to know which environments name their own
62
- // sessions: where one does, the explicit label is simply not used.
63
- const { env, now } = fixture()
64
- expect(
65
- resolveSessionLabel({
66
- env,
67
- now,
68
- sessionId: 'managed-session',
69
- harness: 'opencode',
70
- harnessLabel: 'NotifAI question lifecycle',
71
- explicitLabel: 'Agent-chosen name',
72
- }),
73
- ).toEqual({ ok: true, label: 'NotifAI question lifecycle', source: 'harness' })
74
- })
75
-
76
- it('upgrades a frozen fallback to the environment title even when an explicit label rides along', () => {
77
- const { env, now } = fixture()
78
- const frozen = resolveSessionLabel({ env, now, sessionId: 'late-title' })
79
- expect(frozen).toMatchObject({ ok: true, source: 'fallback' })
80
- expect(
81
- resolveSessionLabel({
82
- env,
83
- now: now + 1,
84
- sessionId: 'late-title',
85
- harness: 'opencode',
86
- harnessLabel: 'Worktree semantic title',
87
- explicitLabel: 'Agent-chosen name',
88
- }),
89
- ).toEqual({ ok: true, label: 'Worktree semantic title', source: 'harness' })
90
- })
91
-
92
- it('uses a trusted harness title when no explicit task label exists', () => {
93
- const { env, now } = fixture()
94
- expect(
95
- resolveSessionLabel({
96
- env,
97
- now,
98
- sessionId: 'opencode-session',
99
- harness: 'opencode',
100
- harnessLabel: 'NotifAI question lifecycle',
101
- }),
102
- ).toEqual({ ok: true, label: 'NotifAI question lifecycle', source: 'harness' })
103
- })
104
-
105
- it('replaces a frozen generated fallback when a managed semantic title appears later', () => {
106
- const { env, now } = fixture()
107
- const frozen = resolveSessionLabel({
108
- env,
109
- now,
110
- sessionId: 'claude-session-before-orca-metadata',
111
- harness: 'claude-code',
112
- })
113
- expect(frozen.ok && frozen.source).toBe('fallback')
114
-
115
- expect(
116
- resolveSessionLabel({
117
- env,
118
- now: now + 1_000,
119
- sessionId: 'claude-session-before-orca-metadata',
120
- harness: 'claude-code',
121
- harnessLabel: 'Worker - semantic session implementation',
122
- }),
123
- ).toEqual({
124
- ok: true,
125
- label: 'Worker - semantic session implementation',
126
- source: 'harness',
127
- })
128
- })
129
-
130
- it('replaces a frozen generated fallback with a later explicit task name, once', () => {
131
- const { env, now } = fixture()
132
- const frozen = resolveSessionLabel({ env, now, sessionId: 'late-named', harness: 'codex' })
133
- expect(frozen.ok && frozen.source).toBe('fallback')
134
-
135
- expect(
136
- resolveSessionLabel({
137
- env,
138
- now: now + 1_000,
139
- sessionId: 'late-named',
140
- harness: 'codex',
141
- explicitLabel: 'Fix checkout retries',
142
- }),
143
- ).toEqual({ ok: true, label: 'Fix checkout retries', source: 'explicit' })
144
- expect(
145
- resolveSessionLabel({
146
- env,
147
- now: now + 2_000,
148
- sessionId: 'late-named',
149
- harness: 'codex',
150
- explicitLabel: 'A different later name',
151
- }),
152
- ).toEqual({ ok: true, label: 'Fix checkout retries', source: 'explicit' })
153
- })
154
-
155
- it('disambiguates an upgraded name against every other stored session', () => {
156
- const { env, now } = fixture()
157
- expect(
158
- resolveSessionLabel({
159
- env,
160
- now,
161
- sessionId: 'other-session',
162
- harness: 'codex',
163
- explicitLabel: 'Release preparation',
164
- }),
165
- ).toEqual({ ok: true, label: 'Release preparation', source: 'explicit' })
166
- const frozen = resolveSessionLabel({ env, now, sessionId: 'upgraded', harness: 'codex' })
167
- expect(frozen.ok && frozen.source).toBe('fallback')
168
-
169
- expect(
170
- resolveSessionLabel({
171
- env,
172
- now: now + 1_000,
173
- sessionId: 'upgraded',
174
- harness: 'codex',
175
- explicitLabel: 'Release preparation',
176
- }),
177
- ).toEqual({ ok: true, label: 'Release preparation ยท Codex', source: 'explicit' })
178
- })
179
-
180
- it('rejects an invalid explicit name instead of silently keeping the fallback', () => {
181
- const { env, now } = fixture()
182
- const frozen = resolveSessionLabel({ env, now, sessionId: 'still-fallback', harness: 'codex' })
183
- expect(frozen.ok && frozen.source).toBe('fallback')
184
-
185
- expect(
186
- resolveSessionLabel({
187
- env,
188
- now: now + 1_000,
189
- sessionId: 'still-fallback',
190
- harness: 'codex',
191
- explicitLabel: 'x'.repeat(65),
192
- }),
193
- ).toEqual({
194
- ok: false,
195
- error: '--session-label (or NOTIFAI_SESSION_LABEL) must be at most 64 characters.',
196
- })
197
- expect(
198
- resolveSessionLabel({ env, now: now + 2_000, sessionId: 'still-fallback', harness: 'codex' }),
199
- ).toEqual(frozen)
200
- })
201
-
202
- it('does not freeze OpenCode placeholder titles before the semantic title arrives', () => {
203
- const { env, now } = fixture()
204
- const file = path.join(stateDir(env), 'session-labels.json')
205
- expect(
206
- resolveSessionLabel({
207
- env,
208
- now,
209
- sessionId: 'opencode-pending',
210
- harness: 'opencode',
211
- harnessLabel: 'New session - 2026-08-20T13:05:00.000Z',
212
- harnessLabelPending: true,
213
- }),
214
- ).toEqual({
215
- ok: false,
216
- error:
217
- "OpenCode is still generating this session's title; retry shortly or pass --session-label.",
218
- })
219
- expect(existsSync(file)).toBe(false)
220
-
221
- expect(
222
- resolveSessionLabel({
223
- env,
224
- now: now + 1_000,
225
- sessionId: 'opencode-pending',
226
- harness: 'opencode',
227
- harnessLabel: 'Semantic session names',
228
- }),
229
- ).toEqual({ ok: true, label: 'Semantic session names', source: 'harness' })
230
- })
231
-
232
- it('allows an explicit task name while a native title is still pending', () => {
233
- const { env, now } = fixture()
234
- expect(
235
- resolveSessionLabel({
236
- env,
237
- now,
238
- sessionId: 'opencode-pending',
239
- harness: 'opencode',
240
- harnessLabelPending: true,
241
- explicitLabel: 'Verify release candidate',
242
- }),
243
- ).toEqual({ ok: true, label: 'Verify release candidate', source: 'explicit' })
244
- })
245
-
246
- it('falls back to a stable generated name without exposing the session id', () => {
247
- const { env, now } = fixture()
248
- const resolved = resolveSessionLabel({
249
- env,
250
- now,
251
- sessionId: 'opaque-thread-1234567890',
252
- harness: 'codex',
253
- })
254
- expect(resolved).toEqual({
255
- ok: true,
256
- label: 'Ivory Koala',
257
- source: 'fallback',
258
- })
259
-
260
- const stored = readFileSync(path.join(stateDir(env), 'session-labels.json'), 'utf8')
261
- expect(stored).not.toContain('opaque-thread-1234567890')
262
- })
263
-
264
- it('migrates a frozen date fallback when no semantic candidate exists', () => {
265
- const { env, now } = fixture()
266
- const file = path.join(stateDir(env), 'session-labels.json')
267
- expect(
268
- resolveSessionLabel({ env, now, sessionId: 'opaque-thread-1234567890', harness: 'codex' }),
269
- ).toEqual({ ok: true, label: 'Ivory Koala', source: 'fallback' })
270
-
271
- const store = JSON.parse(readFileSync(file, 'utf8')) as {
272
- sessions: Record<string, { label: string }>
273
- }
274
- const key = Object.keys(store.sessions)[0]
275
- const record = key === undefined ? undefined : store.sessions[key]
276
- if (record === undefined) throw new Error('expected one stored session')
277
- record.label = 'Codex session ยท Aug 20, 2026 14:05'
278
- writeFileSync(file, `${JSON.stringify(store, null, 2)}\n`)
279
-
280
- expect(
281
- resolveSessionLabel({
282
- env,
283
- now: now + 1_000,
284
- sessionId: 'opaque-thread-1234567890',
285
- harness: 'codex',
286
- }),
287
- ).toEqual({ ok: true, label: 'Ivory Koala', source: 'fallback' })
288
- expect(readFileSync(file, 'utf8')).not.toContain('Codex session ยท')
289
-
290
- expect(
291
- resolveSessionLabel({
292
- env,
293
- now: now + 2_000,
294
- sessionId: 'opaque-thread-1234567890',
295
- harness: 'codex',
296
- explicitLabel: 'Late semantic override',
297
- }),
298
- ).toEqual({ ok: true, label: 'Late semantic override', source: 'explicit' })
299
- })
300
-
301
- it('disambiguates repeated semantic titles without changing session identity', () => {
302
- const { env, now } = fixture()
303
- const labels = ['one', 'two', 'three', 'four'].map((sessionId) =>
304
- resolveSessionLabel({
305
- env,
306
- now,
307
- sessionId,
308
- harness: 'codex',
309
- explicitLabel: 'Release preparation',
310
- }),
311
- )
312
- expect(labels).toEqual([
313
- { ok: true, label: 'Release preparation', source: 'explicit' },
314
- { ok: true, label: 'Release preparation ยท Codex', source: 'explicit' },
315
- {
316
- ok: true,
317
- label: 'Release preparation ยท Codex ยท Aug 20, 2026 14:05',
318
- source: 'explicit',
319
- },
320
- {
321
- ok: true,
322
- label: 'Release preparation ยท Codex ยท Aug 20, 2026 14:05 ยท 2',
323
- source: 'explicit',
324
- },
325
- ])
326
- })
327
-
328
- it('adds only an ordinal when generated fallback labels collide', () => {
329
- const { env, now } = fixture()
330
- const first = resolveSessionLabel({ env, now, sessionId: 'collision-8', harness: 'codex' })
331
- const second = resolveSessionLabel({
332
- env,
333
- now,
334
- sessionId: 'collision-112',
335
- harness: 'codex',
336
- })
337
- expect(first).toEqual({
338
- ok: true,
339
- label: 'Winter Dolphin',
340
- source: 'fallback',
341
- })
342
- expect(second).toEqual({
343
- ok: true,
344
- label: 'Winter Dolphin ยท 2',
345
- source: 'fallback',
346
- })
347
- })
348
-
349
- it('keeps astral native and disambiguated labels inside the wire bound', () => {
350
- const { env, now } = fixture()
351
- const long = '๐Ÿ˜€'.repeat(100)
352
- const first = resolveSessionLabel({
353
- env,
354
- now,
355
- sessionId: 'first',
356
- harness: 'opencode',
357
- harnessLabel: long,
358
- })
359
- const second = resolveSessionLabel({
360
- env,
361
- now,
362
- sessionId: 'second',
363
- harness: 'opencode',
364
- harnessLabel: long,
365
- })
366
- expect(first.ok && first.label.length).toBeLessThanOrEqual(64)
367
- expect(second.ok && second.label.length).toBeLessThanOrEqual(64)
368
- expect(first.ok && first.label.endsWith('โ€ฆ')).toBe(true)
369
- })
370
-
371
- it('rejects blank and oversized explicit labels before storing them', () => {
372
- const { env, now } = fixture()
373
- expect(
374
- resolveSessionLabel({ env, now, sessionId: 'blank', explicitLabel: ' ' }),
375
- ).toEqual({
376
- ok: false,
377
- error: '--session-label (or NOTIFAI_SESSION_LABEL) must not be empty.',
378
- })
379
- expect(
380
- resolveSessionLabel({ env, now, sessionId: 'long', explicitLabel: 'x'.repeat(65) }),
381
- ).toEqual({
382
- ok: false,
383
- error: '--session-label (or NOTIFAI_SESSION_LABEL) must be at most 64 characters.',
384
- })
385
- expect(
386
- resolveSessionLabel({ env, now, sessionId: 'emoji', explicitLabel: '๐Ÿ˜€'.repeat(33) }),
387
- ).toEqual({
388
- ok: false,
389
- error: '--session-label (or NOTIFAI_SESSION_LABEL) must be at most 64 characters.',
390
- })
391
- })
392
-
393
- it('rejects explicit identifiers and paths and never promotes unsafe native titles', () => {
394
- const { env, now } = fixture()
395
- env['HOME'] = '/Users/example'
396
- const error =
397
- '--session-label (or NOTIFAI_SESSION_LABEL) must not contain a session identifier, hash, or filesystem path.'
398
-
399
- expect(
400
- resolveSessionLabel({
401
- env,
402
- now,
403
- sessionId: 'ses_opaque123456',
404
- explicitLabel: 'ses_opaque123456',
405
- }),
406
- ).toEqual({ ok: false, error })
407
- expect(
408
- resolveSessionLabel({
409
- env,
410
- now,
411
- sessionId: 'safe-session',
412
- explicitLabel: 'Fix /Users/example/private-client',
413
- }),
414
- ).toEqual({ ok: false, error })
415
- expect(
416
- resolveSessionLabel({
417
- env,
418
- now,
419
- sessionId: 'native-session',
420
- harness: 'opencode',
421
- harnessLabel: 'Fix /Users/example/private-client',
422
- }),
423
- ).toEqual({
424
- ok: true,
425
- label: 'Violet Crane',
426
- source: 'fallback',
427
- })
428
- })
429
-
430
- it('isolates an unreadable name store before creating a clean one', () => {
431
- const { env, now } = fixture()
432
- const file = path.join(stateDir(env), 'session-labels.json')
433
- mkdirSync(path.dirname(file), { recursive: true })
434
- writeFileSync(file, '{not json}\n')
435
-
436
- expect(
437
- resolveSessionLabel({
438
- env,
439
- now,
440
- sessionId: 'new-session',
441
- explicitLabel: 'Do not overwrite me',
442
- }),
443
- ).toEqual({ ok: true, label: 'Do not overwrite me', source: 'explicit' })
444
- expect(readFileSync(file, 'utf8')).not.toBe('{not json}\n')
445
- const backups = readdirSync(path.dirname(file)).filter((name) =>
446
- /^session-labels\.invalid-[a-f0-9]{64}\.json$/u.test(name),
447
- )
448
- expect(backups).toHaveLength(1)
449
- expect(readFileSync(path.join(path.dirname(file), backups[0]!), 'utf8')).toBe('{not json}\n')
450
- })
451
-
452
- it('formats first seen using the machine local calendar', () => {
453
- const now = new Date(2026, 0, 2, 3, 4).getTime()
454
- expect(formatSessionFirstSeen(now)).toBe('Jan 2, 2026 03:04')
455
- })
456
-
457
- it('explicitly replaces a semantic or fallback label after an authorized rename', () => {
458
- const { env, now } = fixture()
459
- resolveSessionLabel({
460
- env,
461
- now,
462
- sessionId: 'rename-me',
463
- harness: 'codex',
464
- explicitLabel: 'Old job',
465
- })
466
-
467
- expect(
468
- renameStoredSessionLabel({
469
- env,
470
- now: now + 1_000,
471
- sessionId: 'rename-me',
472
- harness: 'codex',
473
- label: ' Completely different job ',
474
- }),
475
- ).toEqual({ ok: true, label: 'Completely different job', source: 'explicit' })
476
- expect(
477
- resolveSessionLabel({
478
- env,
479
- now: now + 2_000,
480
- sessionId: 'rename-me',
481
- harness: 'codex',
482
- harnessLabel: 'Later task context',
483
- }),
484
- ).toEqual({
485
- ok: true,
486
- label: 'Completely different job',
487
- source: 'explicit',
488
- })
489
- })
490
- })