@raidiant/notifai 6.0.0 → 7.0.1

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 (54) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/atomic-file.js +13 -0
  3. package/dist/atomic-file.js.map +1 -1
  4. package/dist/commands-core.d.ts +3 -0
  5. package/dist/commands-core.js +23 -4
  6. package/dist/commands-core.js.map +1 -1
  7. package/dist/commands-doctor.js +7 -5
  8. package/dist/commands-doctor.js.map +1 -1
  9. package/dist/commands-harness-context.d.ts +4 -0
  10. package/dist/commands-harness-context.js +4 -0
  11. package/dist/commands-harness-context.js.map +1 -1
  12. package/dist/commands-init.js +0 -1
  13. package/dist/commands-init.js.map +1 -1
  14. package/dist/commands-send-support.js +22 -0
  15. package/dist/commands-send-support.js.map +1 -1
  16. package/dist/config.d.ts +3 -3
  17. package/dist/config.js +3 -3
  18. package/dist/generated-session-label.d.ts +2 -0
  19. package/dist/generated-session-label.js +152 -0
  20. package/dist/generated-session-label.js.map +1 -0
  21. package/dist/invocation-context.d.ts +3 -0
  22. package/dist/invocation-context.js +23 -6
  23. package/dist/invocation-context.js.map +1 -1
  24. package/dist/main.js +2 -2
  25. package/dist/main.js.map +1 -1
  26. package/dist/opencode-plugin.js +18 -3
  27. package/dist/opencode-plugin.js.map +1 -1
  28. package/dist/orca-session-title.d.ts +10 -0
  29. package/dist/orca-session-title.js +99 -0
  30. package/dist/orca-session-title.js.map +1 -0
  31. package/dist/session-labels.d.ts +30 -0
  32. package/dist/session-labels.js +278 -0
  33. package/dist/session-labels.js.map +1 -0
  34. package/package.json +2 -2
  35. package/src/atomic-file.ts +12 -0
  36. package/src/commands-core.ts +24 -4
  37. package/src/commands-doctor.ts +7 -6
  38. package/src/commands-harness-context.ts +8 -0
  39. package/src/commands-init.ts +0 -1
  40. package/src/commands-send-support.ts +27 -0
  41. package/src/commands.test.ts +203 -6
  42. package/src/config.ts +3 -3
  43. package/src/generated-session-label.ts +155 -0
  44. package/src/hooks.test.ts +4 -5
  45. package/src/install-hooks.test.ts +84 -1
  46. package/src/invocation-context.test.ts +115 -16
  47. package/src/invocation-context.ts +32 -11
  48. package/src/main.ts +8 -2
  49. package/src/opencode-plugin.ts +18 -3
  50. package/src/orca-session-title.test.ts +95 -0
  51. package/src/orca-session-title.ts +105 -0
  52. package/src/session-labels.test.ts +324 -0
  53. package/src/session-labels.ts +360 -0
  54. package/src/skill.test.ts +15 -0
@@ -0,0 +1,278 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { SESSION_LABEL_MAX_LENGTH } from '@raidiant/notifai-protocol';
5
+ import { atomicWriteFileSync } from './atomic-file.js';
6
+ import { stateDir } from './config.js';
7
+ import { withFileLock } from './file-lock.js';
8
+ import { generatedSessionLabel } from './generated-session-label.js';
9
+ import { HARNESS_LABELS } from './harnesses.js';
10
+ const STORE_VERSION = 1;
11
+ const MONTHS = [
12
+ 'Jan',
13
+ 'Feb',
14
+ 'Mar',
15
+ 'Apr',
16
+ 'May',
17
+ 'Jun',
18
+ 'Jul',
19
+ 'Aug',
20
+ 'Sep',
21
+ 'Oct',
22
+ 'Nov',
23
+ 'Dec',
24
+ ];
25
+ function storePath(env) {
26
+ return path.join(stateDir(env), 'session-labels.json');
27
+ }
28
+ /** The state key cannot reveal the harness's opaque session identifier. */
29
+ function sessionKey(sessionId) {
30
+ return createHash('sha256').update(sessionId).digest('hex');
31
+ }
32
+ function emptyStore() {
33
+ return { version: STORE_VERSION, sessions: {} };
34
+ }
35
+ function storedRecord(candidate) {
36
+ if (typeof candidate !== 'object' || candidate === null)
37
+ return null;
38
+ const value = candidate;
39
+ if (typeof value.label !== 'string' ||
40
+ value.label.length === 0 ||
41
+ value.label.length > SESSION_LABEL_MAX_LENGTH ||
42
+ !['explicit', 'harness', 'fallback'].includes(value.source ?? '') ||
43
+ typeof value.first_seen_at !== 'number' ||
44
+ !Number.isFinite(value.first_seen_at)) {
45
+ return null;
46
+ }
47
+ const harness = value.harness;
48
+ if (harness !== undefined && !Object.hasOwn(HARNESS_LABELS, harness))
49
+ return null;
50
+ return {
51
+ label: value.label,
52
+ source: value.source,
53
+ first_seen_at: value.first_seen_at,
54
+ ...(harness === undefined ? {} : { harness }),
55
+ };
56
+ }
57
+ function readStore(file) {
58
+ if (!existsSync(file))
59
+ return emptyStore();
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(readFileSync(file, 'utf8'));
63
+ }
64
+ catch (err) {
65
+ throw new Error(`the session-name store is unreadable: ${err instanceof Error ? err.message : String(err)}`);
66
+ }
67
+ if (typeof parsed !== 'object' || parsed === null) {
68
+ throw new Error('the session-name store is not a JSON object');
69
+ }
70
+ const raw = parsed;
71
+ if (raw.version !== STORE_VERSION) {
72
+ throw new Error(`unsupported session-name store version ${String(raw.version)}`);
73
+ }
74
+ if (typeof raw.sessions !== 'object' || raw.sessions === null) {
75
+ throw new Error('the session-name store has no sessions object');
76
+ }
77
+ const sessions = {};
78
+ for (const [key, candidate] of Object.entries(raw.sessions)) {
79
+ const record = storedRecord(candidate);
80
+ if (!/^[a-f0-9]{64}$/.test(key) || record === null) {
81
+ throw new Error('the session-name store contains an invalid record');
82
+ }
83
+ sessions[key] = record;
84
+ }
85
+ return { version: STORE_VERSION, sessions };
86
+ }
87
+ function writeStore(file, store) {
88
+ atomicWriteFileSync(file, `${JSON.stringify(store, null, 2)}\n`);
89
+ }
90
+ function normalizeWhitespace(value) {
91
+ return value.trim().replaceAll(/\s+/gu, ' ');
92
+ }
93
+ /** Match the same UTF-16 length TypeBox enforces at the wire boundary. */
94
+ function truncate(value, maxLength) {
95
+ if (value.length <= maxLength)
96
+ return value;
97
+ let prefix = value.slice(0, Math.max(0, maxLength - 1));
98
+ const last = prefix.charCodeAt(prefix.length - 1);
99
+ if (last >= 0xd800 && last <= 0xdbff)
100
+ prefix = prefix.slice(0, -1);
101
+ return `${prefix}…`;
102
+ }
103
+ const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/iu;
104
+ const HASH_PATTERN = /\b[0-9a-f]{12,}\b/iu;
105
+ const HARNESS_ID_PATTERN = /\b(?:ses|thread)_[A-Za-z0-9_-]{8,}\b/iu;
106
+ const FILESYSTEM_PATH_PATTERN = /(?:^|[\s("'=:])(?:~[\\/]|[A-Za-z]:[\\/]|\\\\|\/(?:Users|home|private|tmp|var|Volumes|opt|etc|usr|workspace|workspaces)(?:[\\/]|$))/u;
107
+ const OPENCODE_PENDING_TITLE = /^New session(?:\s*-\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)?$/iu;
108
+ function containsPrivatePath(value, env) {
109
+ if (FILESYSTEM_PATH_PATTERN.test(value))
110
+ return true;
111
+ const folded = value.toLocaleLowerCase('en-US');
112
+ for (const key of ['HOME', 'USERPROFILE', 'TMPDIR']) {
113
+ const candidate = env[key]?.trim().replace(/[\\/]+$/u, '');
114
+ if (candidate !== undefined &&
115
+ candidate.length > 1 &&
116
+ folded.includes(candidate.toLocaleLowerCase('en-US'))) {
117
+ return true;
118
+ }
119
+ }
120
+ return false;
121
+ }
122
+ function violatesLabelPrivacy(value, input) {
123
+ const sessionId = input.sessionId.trim();
124
+ const exposesCurrentId = sessionId.length > 0 &&
125
+ (value === sessionId || (sessionId.length >= 8 && value.includes(sessionId)));
126
+ return (exposesCurrentId ||
127
+ UUID_PATTERN.test(value) ||
128
+ HASH_PATTERN.test(value) ||
129
+ HARNESS_ID_PATTERN.test(value) ||
130
+ containsPrivatePath(value, input.env));
131
+ }
132
+ function explicitCandidate(value, input) {
133
+ if (value === undefined)
134
+ return null;
135
+ const normalized = normalizeWhitespace(value);
136
+ if (normalized.length === 0) {
137
+ return { error: '--session-label (or NOTIFAI_SESSION_LABEL) must not be empty.' };
138
+ }
139
+ if (normalized.length > SESSION_LABEL_MAX_LENGTH) {
140
+ return {
141
+ error: `--session-label (or NOTIFAI_SESSION_LABEL) must be at most ${SESSION_LABEL_MAX_LENGTH} characters.`,
142
+ };
143
+ }
144
+ if (violatesLabelPrivacy(normalized, input)) {
145
+ return {
146
+ error: '--session-label (or NOTIFAI_SESSION_LABEL) must not contain a session identifier, hash, or filesystem path.',
147
+ };
148
+ }
149
+ return { label: normalized, source: 'explicit' };
150
+ }
151
+ function harnessLabelIsPending(value, input) {
152
+ if (input.harnessLabelPending === true)
153
+ return true;
154
+ if (input.harness !== 'opencode' || value === undefined)
155
+ return false;
156
+ return OPENCODE_PENDING_TITLE.test(normalizeWhitespace(value));
157
+ }
158
+ function harnessCandidate(value, input) {
159
+ if (value === undefined)
160
+ return null;
161
+ const normalized = normalizeWhitespace(value);
162
+ if (normalized.length === 0 || violatesLabelPrivacy(normalized, input))
163
+ return null;
164
+ return {
165
+ label: truncate(normalized, SESSION_LABEL_MAX_LENGTH),
166
+ source: 'harness',
167
+ };
168
+ }
169
+ function pad(value) {
170
+ return String(value).padStart(2, '0');
171
+ }
172
+ /** Stable English shape using the machine's local calendar and clock. */
173
+ export function formatSessionFirstSeen(now) {
174
+ const date = new Date(now);
175
+ return `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
176
+ }
177
+ function fallbackCandidate(sessionId) {
178
+ return {
179
+ label: generatedSessionLabel(sessionId),
180
+ source: 'fallback',
181
+ };
182
+ }
183
+ function isLegacyDateFallback(record) {
184
+ if (record.source !== 'fallback')
185
+ return false;
186
+ const owner = record.harness === undefined ? 'Agent' : HARNESS_LABELS[record.harness];
187
+ return record.label.startsWith(`${owner} session · `);
188
+ }
189
+ function withSuffix(base, suffix) {
190
+ const room = SESSION_LABEL_MAX_LENGTH - suffix.length;
191
+ return `${truncate(base, room)}${suffix}`;
192
+ }
193
+ function collisionKey(value) {
194
+ return value.normalize('NFKC').toLocaleLowerCase('en-US');
195
+ }
196
+ function uniqueLabel(base, source, harness, now, used) {
197
+ if (!used.has(collisionKey(base)))
198
+ return base;
199
+ if (source === 'fallback') {
200
+ for (let ordinal = 2;; ordinal += 1) {
201
+ const candidate = withSuffix(base, ` · ${ordinal}`);
202
+ if (!used.has(collisionKey(candidate)))
203
+ return candidate;
204
+ }
205
+ }
206
+ const owner = harness === undefined ? 'Agent' : HARNESS_LABELS[harness];
207
+ const ownerCandidate = withSuffix(base, ` · ${owner}`);
208
+ if (!used.has(collisionKey(ownerCandidate)))
209
+ return ownerCandidate;
210
+ const timed = withSuffix(base, ` · ${owner} · ${formatSessionFirstSeen(now)}`);
211
+ if (!used.has(collisionKey(timed)))
212
+ return timed;
213
+ for (let ordinal = 2;; ordinal += 1) {
214
+ const candidate = withSuffix(timed, ` · ${ordinal}`);
215
+ if (!used.has(collisionKey(candidate)))
216
+ return candidate;
217
+ }
218
+ }
219
+ /**
220
+ * Freeze the first accepted human name for one immutable session.
221
+ *
222
+ * Explicit agent/User input wins, then a title supplied by a trusted harness
223
+ * adapter, then a generated fallback. Later sends reuse the frozen value
224
+ * even when their branch, worktree, title candidate, or notification changes.
225
+ */
226
+ export function resolveSessionLabel(input) {
227
+ const now = input.now ?? Date.now();
228
+ const file = storePath(input.env);
229
+ const key = sessionKey(input.sessionId);
230
+ try {
231
+ return withFileLock(`${file}.lock`, () => {
232
+ const store = readStore(file);
233
+ const existing = store.sessions[key];
234
+ if (existing !== undefined) {
235
+ if (!isLegacyDateFallback(existing)) {
236
+ return { ok: true, label: existing.label, source: existing.source };
237
+ }
238
+ const used = new Set(Object.entries(store.sessions)
239
+ .filter(([storedKey]) => storedKey !== key)
240
+ .map(([, record]) => collisionKey(record.label)));
241
+ const label = uniqueLabel(generatedSessionLabel(input.sessionId), 'fallback', input.harness ?? existing.harness, now, used);
242
+ store.sessions[key] = { ...existing, label };
243
+ writeStore(file, store);
244
+ return { ok: true, label, source: existing.source };
245
+ }
246
+ const explicit = explicitCandidate(input.explicitLabel, input);
247
+ if (explicit !== null && 'error' in explicit) {
248
+ return { ok: false, error: explicit.error };
249
+ }
250
+ const nativePending = harnessLabelIsPending(input.harnessLabel, input);
251
+ const native = nativePending ? null : harnessCandidate(input.harnessLabel, input);
252
+ if (explicit === null && nativePending) {
253
+ return {
254
+ ok: false,
255
+ error: "OpenCode is still generating this session's title; retry shortly or pass --session-label.",
256
+ };
257
+ }
258
+ const candidate = explicit ?? native ?? fallbackCandidate(input.sessionId);
259
+ const used = new Set(Object.values(store.sessions).map((record) => collisionKey(record.label)));
260
+ const label = uniqueLabel(candidate.label, candidate.source, input.harness, now, used);
261
+ store.sessions[key] = {
262
+ label,
263
+ source: candidate.source,
264
+ first_seen_at: now,
265
+ ...(input.harness === undefined ? {} : { harness: input.harness }),
266
+ };
267
+ writeStore(file, store);
268
+ return { ok: true, label, source: candidate.source };
269
+ });
270
+ }
271
+ catch (err) {
272
+ return {
273
+ ok: false,
274
+ error: `Could not persist this session's name: ${err instanceof Error ? err.message : String(err)}`,
275
+ };
276
+ }
277
+ }
278
+ //# sourceMappingURL=session-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-labels.js","sourceRoot":"","sources":["../src/session-labels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAE,cAAc,EAAgB,MAAM,gBAAgB,CAAA;AAE7D,MAAM,aAAa,GAAG,CAAC,CAAA;AACvB,MAAM,MAAM,GAAG;IACb,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;CACG,CAAA;AAmCV,SAAS,SAAS,CAAC,GAAsB;IACvC,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,qBAAqB,CAAC,CAAA;AACxD,CAAC;AAED,2EAA2E;AAC3E,SAAS,UAAU,CAAC,SAAiB;IACnC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC7D,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAA;AACjD,CAAC;AAED,SAAS,YAAY,CAAC,SAAkB;IACtC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACpE,MAAM,KAAK,GAAG,SAAwC,CAAA;IACtD,IACE,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC/B,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QACxB,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,wBAAwB;QAC7C,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC;QACjE,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ;QACvC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,EACrC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAC7B,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IACjF,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM,EAAE,KAAK,CAAC,MAA4B;QAC1C,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;KAC9C,CAAA;AACH,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,UAAU,EAAE,CAAA;IAC1C,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;IACjD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,yCAAyC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC5F,CAAA;IACH,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;IAChE,CAAC;IACD,MAAM,GAAG,GAAG,MAAmD,CAAA;IAC/D,IAAI,GAAG,CAAC,OAAO,KAAK,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,0CAA0C,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAClF,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAClE,CAAC;IACD,MAAM,QAAQ,GAAuC,EAAE,CAAA;IACvD,KAAK,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,CAAA;QACtC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAA;QACtE,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,GAAG,MAAM,CAAA;IACxB,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAA;AAC7C,CAAC;AAED,SAAS,UAAU,CAAC,IAAY,EAAE,KAAwB;IACxD,mBAAmB,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;AAClE,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAa;IACxC,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAC9C,CAAC;AAED,0EAA0E;AAC1E,SAAS,QAAQ,CAAC,KAAa,EAAE,SAAiB;IAChD,IAAI,KAAK,CAAC,MAAM,IAAI,SAAS;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,CAAA;IACvD,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACjD,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM;QAAE,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAClE,OAAO,GAAG,MAAM,GAAG,CAAA;AACrB,CAAC;AAED,MAAM,YAAY,GAChB,oEAAoE,CAAA;AACtE,MAAM,YAAY,GAAG,qBAAqB,CAAA;AAC1C,MAAM,kBAAkB,GAAG,wCAAwC,CAAA;AACnE,MAAM,uBAAuB,GAC3B,qIAAqI,CAAA;AACvI,MAAM,sBAAsB,GAC1B,2EAA2E,CAAA;AAE7E,SAAS,mBAAmB,CAAC,KAAa,EAAE,GAAsB;IAChE,IAAI,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACpD,MAAM,MAAM,GAAG,KAAK,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;IAC/C,KAAK,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,CAAU,EAAE,CAAC;QAC7D,MAAM,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;QAC1D,IACE,SAAS,KAAK,SAAS;YACvB,SAAS,CAAC,MAAM,GAAG,CAAC;YACpB,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,EACrD,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa,EAAE,KAAwB;IACnE,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,CAAA;IACxC,MAAM,gBAAgB,GACpB,SAAS,CAAC,MAAM,GAAG,CAAC;QACpB,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;IAC/E,OAAO,CACL,gBAAgB;QAChB,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC;QACxB,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC;QACxB,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CACtC,CAAA;AACH,CAAC;AAED,SAAS,iBAAiB,CACxB,KAAyB,EACzB,KAAwB;IAExB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACpC,MAAM,UAAU,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAA;IAC7C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,KAAK,EAAE,+DAA+D,EAAE,CAAA;IACnF,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,GAAG,wBAAwB,EAAE,CAAC;QACjD,OAAO;YACL,KAAK,EAAE,8DAA8D,wBAAwB,cAAc;SAC5G,CAAA;IACH,CAAC;IACD,IAAI,oBAAoB,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC;QAC5C,OAAO;YACL,KAAK,EACH,6GAA6G;SAChH,CAAA;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;AAClD,CAAC;AAED,SAAS,qBAAqB,CAC5B,KAAyB,EACzB,KAAwB;IAExB,IAAI,KAAK,CAAC,mBAAmB,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACnD,IAAI,KAAK,CAAC,OAAO,KAAK,UAAU,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACrE,OAAO,sBAAsB,CAAC,IAAI,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAA;AAChE,CAAC;AAED,SAAS,gBAAgB,CACvB,KAAyB,EACzB,KAAwB;IAExB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACpC,MAAM,UAAU,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAA;IAC7C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,oBAAoB,CAAC,UAAU,EAAE,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnF,OAAO;QACL,KAAK,EAAE,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC;QACrD,MAAM,EAAE,SAAS;KAClB,CAAA;AACH,CAAC;AAED,SAAS,GAAG,CAAC,KAAa;IACxB,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AACvC,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAChD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;IAC1B,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,EAAE,CAAA;AAChI,CAAC;AAED,SAAS,iBAAiB,CAAC,SAAiB;IAC1C,OAAO;QACL,KAAK,EAAE,qBAAqB,CAAC,SAAS,CAAC;QACvC,MAAM,EAAE,UAAU;KACnB,CAAA;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,MAA0B;IACtD,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,KAAK,CAAA;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACrF,OAAO,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,KAAK,aAAa,CAAC,CAAA;AACvD,CAAC;AAED,SAAS,UAAU,CAAC,IAAY,EAAE,MAAc;IAC9C,MAAM,IAAI,GAAG,wBAAwB,GAAG,MAAM,CAAC,MAAM,CAAA;IACrD,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,EAAE,CAAA;AAC3C,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;AAC3D,CAAC;AAED,SAAS,WAAW,CAClB,IAAY,EACZ,MAA0B,EAC1B,OAA4B,EAC5B,GAAW,EACX,IAAyB;IAEzB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IAE9C,IAAI,MAAM,KAAK,UAAU,EAAE,CAAC;QAC1B,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC;YACrC,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,OAAO,EAAE,CAAC,CAAA;YACnD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;gBAAE,OAAO,SAAS,CAAA;QAC1D,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAA;IACvE,MAAM,cAAc,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,KAAK,EAAE,CAAC,CAAA;IACtD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;QAAE,OAAO,cAAc,CAAA;IAElE,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,sBAAsB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC9E,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IAEhD,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,EAAE,MAAM,OAAO,EAAE,CAAC,CAAA;QACpD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YAAE,OAAO,SAAS,CAAA;IAC1D,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAwB;IAC1D,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAA;IACnC,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAEvC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,GAAG,IAAI,OAAO,EAAE,GAAG,EAAE;YACvC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;YAC7B,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;YACpC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACpC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;gBACrE,CAAC;gBACD,MAAM,IAAI,GAAG,IAAI,GAAG,CAClB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;qBAC3B,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,SAAS,KAAK,GAAG,CAAC;qBAC1C,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CACnD,CAAA;gBACD,MAAM,KAAK,GAAG,WAAW,CACvB,qBAAqB,CAAC,KAAK,CAAC,SAAS,CAAC,EACtC,UAAU,EACV,KAAK,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,EACjC,GAAG,EACH,IAAI,CACL,CAAA;gBACD,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAA;gBAC5C,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;gBACvB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;YACrD,CAAC;YAED,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;YAC9D,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC7C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAA;YAC7C,CAAC;YACD,MAAM,aAAa,GAAG,qBAAqB,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,CAAA;YACtE,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,CAAA;YACjF,IAAI,QAAQ,KAAK,IAAI,IAAI,aAAa,EAAE,CAAC;gBACvC,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,KAAK,EACH,2FAA2F;iBAC9F,CAAA;YACH,CAAC;YACD,MAAM,SAAS,GACb,QAAQ,IAAI,MAAM,IAAI,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;YAC1D,MAAM,IAAI,GAAG,IAAI,GAAG,CAClB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAC1E,CAAA;YACD,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;YACtF,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG;gBACpB,KAAK;gBACL,MAAM,EAAE,SAAS,CAAC,MAAM;gBACxB,aAAa,EAAE,GAAG;gBAClB,GAAG,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;aACnE,CAAA;YACD,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;YACvB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAA;QACtD,CAAC,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,0CAA0C,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;SACpG,CAAA;IACH,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raidiant/notifai",
3
- "version": "6.0.0",
3
+ "version": "7.0.1",
4
4
  "type": "module",
5
5
  "description": "Send native phone notifications from agents and local programs",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@clack/prompts": "^1.7.0",
24
- "@raidiant/notifai-protocol": "3.0.0",
24
+ "@raidiant/notifai-protocol": "4.0.0",
25
25
  "commander": "^14.0.0",
26
26
  "picocolors": "^1.1.1",
27
27
  "smol-toml": "^1.4.0"
@@ -57,6 +57,7 @@ export function atomicWriteFileSync(
57
57
  assertSameDirectory(directory, parent)
58
58
  assertUnchangedTarget(file, target)
59
59
  renameSync(temp, file)
60
+ syncDirectory(directory)
60
61
  } catch (err) {
61
62
  if (handle !== undefined) closeSync(handle)
62
63
  try {
@@ -68,6 +69,17 @@ export function atomicWriteFileSync(
68
69
  }
69
70
  }
70
71
 
72
+ /** Persist the published directory entry where the host supports directory fsync. */
73
+ function syncDirectory(directory: string): void {
74
+ if (process.platform === 'win32') return
75
+ const handle = openSync(directory, 'r')
76
+ try {
77
+ fsyncSync(handle)
78
+ } finally {
79
+ closeSync(handle)
80
+ }
81
+ }
82
+
71
83
  /** Mode to preserve, or the mode for a new file. Refuses a non-regular target. */
72
84
  interface TargetMetadata {
73
85
  mode: number
@@ -13,6 +13,7 @@ import type { CredentialStore, MachineCredential } from './credentials.js'
13
13
  import { type HookAdapterTarget } from './hook-adapter.js'
14
14
  import { logConfigResolved, logSettingsFrom, nullLogger, type Logger } from './logging.js'
15
15
  import type { NativeSkills } from './native-skills.js'
16
+ import type { OrcaSessionTitleLookup } from './orca-session-title.js'
16
17
  import { packageVersion } from './release.js'
17
18
  import type { Tone } from './ui/theme.js'
18
19
 
@@ -81,6 +82,8 @@ export interface CommandDeps {
81
82
  codexSourcePid?: number
82
83
  /** Test seam and production adapter for the external native skills installer. */
83
84
  nativeSkills?: NativeSkills
85
+ /** Test seam for Orca's exact-worktree semantic title lookup. */
86
+ orcaSessionTitle?: OrcaSessionTitleLookup
84
87
  /**
85
88
  * The local record of what this invocation did. Optional so a test fake need
86
89
  * not carry one; `log(deps)` supplies a logger that records nothing.
@@ -151,12 +154,29 @@ export function authedClient(deps: CommandDeps, config: CliConfig): { client: Ap
151
154
 
152
155
  export const UPDATE_CLI_COMMAND = 'npm install -g @raidiant/notifai'
153
156
 
154
- function localRecovery(action: RecoveryAction | null): string | null {
157
+ function affectedDeviceNames(details: unknown): string[] {
158
+ if (typeof details !== 'object' || details === null || !('device_names' in details)) return []
159
+ const names = (details as { device_names?: unknown }).device_names
160
+ if (!Array.isArray(names)) return []
161
+ return [...new Set(names.filter((name): name is string => typeof name === 'string' && name.trim() !== ''))]
162
+ }
163
+
164
+ function naturalList(values: readonly string[]): string {
165
+ if (values.length <= 1) return values[0] ?? ''
166
+ if (values.length === 2) return `${values[0]} and ${values[1]}`
167
+ return `${values.slice(0, -1).join(', ')}, and ${values.at(-1)}`
168
+ }
169
+
170
+ function localRecovery(action: RecoveryAction | null, details: unknown): string | null {
155
171
  switch (action) {
156
172
  case 'update_cli':
157
173
  return `next: ${UPDATE_CLI_COMMAND}`
158
- case 'update_companion':
159
- return 'next: Update Notifai on the named device.'
174
+ case 'update_companion': {
175
+ const names = affectedDeviceNames(details)
176
+ return names.length > 0
177
+ ? `next: Update Notifai on ${naturalList(names)}.`
178
+ : 'next: Update Notifai on the affected devices.'
179
+ }
160
180
  case 'wait_for_service':
161
181
  return 'next: The service is being updated; try again later.'
162
182
  default:
@@ -213,7 +233,7 @@ export function reportError(
213
233
  'disagree about the contract; check with `notifai doctor`',
214
234
  )
215
235
  }
216
- const recovery = localRecovery(err.recoveryAction)
236
+ const recovery = localRecovery(err.recoveryAction, err.details)
217
237
  if (recovery !== null) deps.io.err(recovery)
218
238
  else if (err.recoveryAction === null && err.nextAction) deps.io.err(`next: ${err.nextAction}`)
219
239
  if (err.code === 'auth_required' || err.code === 'machine_revoked') return EXIT.auth
@@ -589,13 +589,14 @@ export async function doctorCommand(
589
589
  deps.io.out(`${mark} ${line(s)}`)
590
590
  }
591
591
  }
592
- await deps.io.outro?.(
593
- ok
594
- ? 'Everything looks good'
595
- : blocker?.id === 'contract'
596
- ? 'Update Notifai, then run doctor again'
597
- : `Start with: ${remedyLine(blocker)}`,
592
+ const surfacedUpdate = readiness.states.some(
593
+ (state) => state.id === 'contract' && (state.status === 'gap' || state.status === 'optional-gap'),
598
594
  )
595
+ if (blocker !== null && blocker.id !== 'contract') {
596
+ await deps.io.outro?.(`Start with: ${remedyLine(blocker)}`)
597
+ } else if (!surfacedUpdate) {
598
+ await deps.io.outro?.('Everything looks good')
599
+ }
599
600
  return ok ? EXIT.ok : EXIT.failed
600
601
  }
601
602
 
@@ -12,6 +12,10 @@ export interface ActiveHarnessSession {
12
12
  harness: Harness
13
13
  label: string
14
14
  sessionId?: string
15
+ /** Trusted human title published by this harness's managed adapter. */
16
+ sessionLabel?: string
17
+ /** The harness has only a temporary placeholder title so far. */
18
+ sessionLabelPending?: boolean
15
19
  }
16
20
 
17
21
  /**
@@ -26,10 +30,14 @@ function harnessEnvCandidates(env: NodeJS.ProcessEnv): ActiveHarnessSession[] {
26
30
  const candidates: ActiveHarnessSession[] = []
27
31
  if (env['NOTIFAI_ACTIVE_HARNESS'] === 'opencode') {
28
32
  const sessionId = env['NOTIFAI_ACTIVE_SESSION_ID']
33
+ const sessionLabel = env['NOTIFAI_ACTIVE_SESSION_LABEL']
34
+ const sessionLabelPending = env['NOTIFAI_ACTIVE_SESSION_LABEL_PENDING'] === '1'
29
35
  candidates.push({
30
36
  harness: 'opencode',
31
37
  label: 'OpenCode',
32
38
  ...(sessionId === undefined || sessionId === '' ? {} : { sessionId }),
39
+ ...(sessionLabel === undefined || sessionLabel === '' ? {} : { sessionLabel }),
40
+ ...(sessionLabelPending ? { sessionLabelPending: true } : {}),
33
41
  })
34
42
  }
35
43
  if (env['CLAUDECODE'] === '1') {
@@ -672,7 +672,6 @@ async function printInitClose(
672
672
  if (blocker?.id === 'contract') {
673
673
  deps.io.out(blocker.detail)
674
674
  deps.io.out(UPDATE_CLI_COMMAND)
675
- await deps.io.outro?.('Update Notifai, then run init again')
676
675
  return
677
676
  }
678
677
 
@@ -4,16 +4,40 @@ import { existsSync, readFileSync } from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { isRetryableReplyPollError, type ApiClient } from './client.js'
6
6
  import { buildSourceContext, inferInvocationContext } from './invocation-context.js'
7
+ import { readOrcaSessionTitle } from './orca-session-title.js'
7
8
  import { type DraftInvocation, type SendFlags } from './send.js'
8
9
  import { EXIT, reportError, type CommandDeps } from './commands-core.js'
9
10
  import type { ActiveHarnessSession } from './commands-harness-context.js'
10
11
 
12
+ function managedSessionTitle(
13
+ deps: CommandDeps,
14
+ flags: Pick<SendFlags, 'sessionLabel'>,
15
+ active: ActiveHarnessSession | null,
16
+ ): string | undefined {
17
+ if (active?.sessionLabel !== undefined) return active.sessionLabel
18
+ if (
19
+ active?.harness !== 'claude-code' ||
20
+ active.sessionId === undefined ||
21
+ active.sessionId !== deps.env['CLAUDE_CODE_SESSION_ID'] ||
22
+ flags.sessionLabel !== undefined ||
23
+ deps.env['NOTIFAI_SESSION_LABEL'] !== undefined
24
+ ) {
25
+ return undefined
26
+ }
27
+ try {
28
+ return (deps.orcaSessionTitle ?? readOrcaSessionTitle)(deps.env)
29
+ } catch {
30
+ return undefined
31
+ }
32
+ }
33
+
11
34
  export function resolveDraftInvocation(
12
35
  deps: CommandDeps,
13
36
  flags: Pick<SendFlags, 'sessionId' | 'sessionLabel'>,
14
37
  active: ActiveHarnessSession | null,
15
38
  ): { ok: true; invocation: DraftInvocation } | { ok: false; error: string } {
16
39
  const inferred = inferInvocationContext(deps.cwd)
40
+ const sessionTitle = managedSessionTitle(deps, flags, active)
17
41
  const source = buildSourceContext({
18
42
  env: deps.env,
19
43
  invocation: inferred,
@@ -25,8 +49,11 @@ export function resolveDraftInvocation(
25
49
  activeHarness: {
26
50
  harness: active.harness,
27
51
  ...(active.sessionId === undefined ? {} : { sessionId: active.sessionId }),
52
+ ...(sessionTitle === undefined ? {} : { sessionLabel: sessionTitle }),
53
+ ...(active.sessionLabelPending === true ? { sessionLabelPending: true } : {}),
28
54
  },
29
55
  }),
56
+ now: (deps.now ?? Date.now)(),
30
57
  })
31
58
  if (!source.ok) return source
32
59
  return {