@raidiant/notifai 11.5.0 → 11.6.0-beta.3

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 (93) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/commands-acknowledge.js +5 -2
  3. package/dist/commands-acknowledge.js.map +1 -1
  4. package/dist/commands-ask.js +9 -2
  5. package/dist/commands-ask.js.map +1 -1
  6. package/dist/commands-doctor.js +21 -15
  7. package/dist/commands-doctor.js.map +1 -1
  8. package/dist/commands-hook-attend.js +42 -6
  9. package/dist/commands-hook-attend.js.map +1 -1
  10. package/dist/commands-hook-install.js +1 -1
  11. package/dist/commands-hook-install.js.map +1 -1
  12. package/dist/commands-hook-run.js +79 -8
  13. package/dist/commands-hook-run.js.map +1 -1
  14. package/dist/commands-send-support.js +3 -0
  15. package/dist/commands-send-support.js.map +1 -1
  16. package/dist/commands-update-check.d.ts +1 -1
  17. package/dist/commands-update-check.js +12 -4
  18. package/dist/commands-update-check.js.map +1 -1
  19. package/dist/commands-update-resume.d.ts +7 -0
  20. package/dist/commands-update-resume.js +133 -0
  21. package/dist/commands-update-resume.js.map +1 -0
  22. package/dist/commands-update.js +25 -11
  23. package/dist/commands-update.js.map +1 -1
  24. package/dist/commands.d.ts +1 -0
  25. package/dist/commands.js +1 -0
  26. package/dist/commands.js.map +1 -1
  27. package/dist/harnesses.d.ts +8 -3
  28. package/dist/harnesses.js +16 -4
  29. package/dist/harnesses.js.map +1 -1
  30. package/dist/hermes-attendant.d.ts +38 -0
  31. package/dist/hermes-attendant.js +333 -0
  32. package/dist/hermes-attendant.js.map +1 -0
  33. package/dist/hermes-plugin.js +142 -4
  34. package/dist/hermes-plugin.js.map +1 -1
  35. package/dist/hook-lifecycle.d.ts +1 -1
  36. package/dist/hook-lifecycle.js +22 -11
  37. package/dist/hook-lifecycle.js.map +1 -1
  38. package/dist/hook-types.d.ts +3 -0
  39. package/dist/integration-health.d.ts +26 -0
  40. package/dist/integration-health.js +133 -0
  41. package/dist/integration-health.js.map +1 -0
  42. package/dist/invocation-context.d.ts +1 -0
  43. package/dist/invocation-context.js +3 -0
  44. package/dist/invocation-context.js.map +1 -1
  45. package/dist/native-skills.d.ts +1 -0
  46. package/dist/native-skills.js +30 -3
  47. package/dist/native-skills.js.map +1 -1
  48. package/dist/openclaw-generation.d.ts +5 -1
  49. package/dist/openclaw-generation.js +27 -5
  50. package/dist/openclaw-generation.js.map +1 -1
  51. package/dist/openclaw-message-bridge.d.ts +8 -0
  52. package/dist/openclaw-message-bridge.js +93 -0
  53. package/dist/openclaw-message-bridge.js.map +1 -0
  54. package/dist/openclaw-pending.d.ts +1 -1
  55. package/dist/openclaw-pending.js +1 -6
  56. package/dist/openclaw-pending.js.map +1 -1
  57. package/dist/openclaw-plugin.js +385 -14
  58. package/dist/openclaw-plugin.js.map +1 -1
  59. package/dist/openclaw-session-access.d.ts +2 -0
  60. package/dist/openclaw-session-access.js +10 -0
  61. package/dist/openclaw-session-access.js.map +1 -1
  62. package/dist/program.d.ts +2 -1
  63. package/dist/program.js +17 -10
  64. package/dist/program.js.map +1 -1
  65. package/dist/session-activation.js +1 -0
  66. package/dist/session-activation.js.map +1 -1
  67. package/dist/session-attendant-probe.d.ts +1 -0
  68. package/dist/session-attendant-probe.js +11 -1
  69. package/dist/session-attendant-probe.js.map +1 -1
  70. package/dist/session-attendant-state.d.ts +3 -0
  71. package/dist/session-attendant-state.js +8 -0
  72. package/dist/session-attendant-state.js.map +1 -1
  73. package/dist/session-attendant.d.ts +6 -0
  74. package/dist/session-attendant.js +12 -0
  75. package/dist/session-attendant.js.map +1 -1
  76. package/dist/session-labels.d.ts +2 -0
  77. package/dist/session-labels.js +5 -0
  78. package/dist/session-labels.js.map +1 -1
  79. package/dist/session-message-handoff.d.ts +3 -1
  80. package/dist/session-message-handoff.js +2 -1
  81. package/dist/session-message-handoff.js.map +1 -1
  82. package/dist/skill-integrity.d.ts +8 -3
  83. package/dist/skill-integrity.js +18 -8
  84. package/dist/skill-integrity.js.map +1 -1
  85. package/dist/skill-source/manifest.json +7 -7
  86. package/dist/skill-source/notifai/SKILL.md +66 -61
  87. package/dist/skill-source/notifai/references/diagnostics.md +5 -0
  88. package/dist/skill-source/notifai/references/harness-setup.md +24 -8
  89. package/dist/skill-source/notifai/references/notes-and-edits.md +6 -0
  90. package/dist/skill-source/notifai/references/updates.md +44 -10
  91. package/dist/update-handoff.js +1 -1
  92. package/dist/update-handoff.js.map +1 -1
  93. package/package.json +1 -1
@@ -14,12 +14,140 @@ export function hermesPluginDir(env = process.env) {
14
14
  export function hermesPluginSource(adapterPath, nodePath) {
15
15
  const command = nodePath === undefined ? [adapterPath] : [nodePath, adapterPath];
16
16
  return `${HERMES_PLUGIN_MARKER}
17
- """Notifai activation for a proven local Hermes CLI Agent Session."""
17
+ """Notifai activation and Session Attendant for a local Hermes CLI session."""
18
+ import atexit
18
19
  import json
19
20
  import os
21
+ import select
20
22
  import subprocess
23
+ import threading
24
+ import time
21
25
 
22
26
  COMMAND = ${JSON.stringify(command)}
27
+ _attendants = {}
28
+ _lock = threading.RLock()
29
+
30
+ def _attached_session(ctx):
31
+ cli = getattr(ctx._manager, "_cli_ref", None)
32
+ return str(getattr(cli, "session_id", "") or "") if cli is not None else ""
33
+
34
+ def _run_attendant(ctx, session_id, cwd, stopped):
35
+ proc = None
36
+ wall_anchor = time.time_ns()
37
+ mono_anchor = time.monotonic_ns()
38
+ try:
39
+ proc = subprocess.Popen(COMMAND + ["hook", "hermes-attend", "--owner", "notifai", "--harness", "hermes"],
40
+ cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
41
+ stderr=subprocess.DEVNULL, text=True, bufsize=1)
42
+ with _lock:
43
+ _attendants[session_id] = (stopped, proc)
44
+ def send(frame):
45
+ with _lock:
46
+ if proc.stdin is None or proc.stdin.closed:
47
+ return False
48
+ try:
49
+ proc.stdin.write(json.dumps(frame, ensure_ascii=False) + "\\n")
50
+ proc.stdin.flush()
51
+ return True
52
+ except (OSError, ValueError):
53
+ return False
54
+ if not send({"type": "hello", "session_id": session_id, "cwd": cwd, "pid": os.getpid()}):
55
+ return
56
+ pending = bytearray()
57
+ while not stopped.is_set() and proc.poll() is None:
58
+ with _lock:
59
+ current = _attached_session(ctx)
60
+ working = bool(getattr(ctx._manager._cli_ref, "_agent_running", False)) if current == session_id else False
61
+ if current != session_id:
62
+ break
63
+ if not send({"type": "state", "session_id": session_id,
64
+ "activity": "working" if working else "idle"}):
65
+ break
66
+ readable, _, _ = select.select([proc.stdout], [], [], 1.0)
67
+ if not readable:
68
+ continue
69
+ chunk = os.read(proc.stdout.fileno(), 65536)
70
+ if not chunk:
71
+ break
72
+ pending.extend(chunk)
73
+ if len(pending) > 2 * 1024 * 1024:
74
+ break
75
+ while b"\\n" in pending:
76
+ raw, _, rest = pending.partition(b"\\n")
77
+ pending = bytearray(rest)
78
+ try:
79
+ frame = json.loads(raw.decode("utf-8"))
80
+ except (UnicodeDecodeError, ValueError):
81
+ continue
82
+ if frame.get("type") != "write" or not isinstance(frame.get("id"), int):
83
+ continue
84
+ accepted = False
85
+ with _lock:
86
+ wall_now = time.time_ns()
87
+ mono_now = time.monotonic_ns()
88
+ # Fail closed after a backward wall-clock step. Node's
89
+ # deadline is wall time because its monotonic origin may
90
+ # differ from Python's on the same macOS host.
91
+ clock_stable = wall_now - wall_anchor + 100_000_000 >= mono_now - mono_anchor
92
+ deadline = frame.get("deadline_ms")
93
+ if (not stopped.is_set() and frame.get("session_id") == session_id and
94
+ _attached_session(ctx) == session_id and
95
+ type(deadline) is int and wall_now < deadline * 1_000_000 and clock_stable):
96
+ try:
97
+ accepted = bool(ctx.inject_message(frame.get("text", "")))
98
+ except Exception:
99
+ pass
100
+ send({"type": "result", "id": frame["id"], "accepted": accepted,
101
+ "reason": "session-replaced" if not accepted else ""})
102
+ send({"type": "end", "session_id": session_id})
103
+ except (OSError, ValueError):
104
+ pass
105
+ finally:
106
+ with _lock:
107
+ if _attendants.get(session_id, (None, None))[0] is stopped:
108
+ _attendants.pop(session_id, None)
109
+ if proc is not None:
110
+ try:
111
+ if proc.stdin is not None:
112
+ proc.stdin.close()
113
+ proc.wait(timeout=4)
114
+ except (OSError, subprocess.TimeoutExpired):
115
+ proc.terminate()
116
+ try:
117
+ proc.wait(timeout=2)
118
+ except subprocess.TimeoutExpired:
119
+ proc.kill()
120
+
121
+ def _start_attendant(ctx, session_id, cwd):
122
+ if os.name == "nt":
123
+ return
124
+ with _lock:
125
+ if _attached_session(ctx) != session_id or os.environ.get("HERMES_SESSION_ID") != session_id:
126
+ return
127
+ if session_id in _attendants:
128
+ return
129
+ stopped = threading.Event()
130
+ _attendants[session_id] = (stopped, None)
131
+ threading.Thread(target=_run_attendant, args=(ctx, session_id, cwd, stopped),
132
+ daemon=True, name="notifai-hermes-attendant").start()
133
+
134
+ def _stop_attendant(session_id):
135
+ with _lock:
136
+ item = _attendants.get(session_id)
137
+ if item is None:
138
+ return
139
+ stopped, proc = item
140
+ stopped.set()
141
+ if proc is not None and proc.stdin is not None and not proc.stdin.closed:
142
+ try:
143
+ proc.stdin.write(json.dumps({"type": "end", "session_id": session_id}) + "\\n")
144
+ proc.stdin.flush()
145
+ except (OSError, ValueError):
146
+ pass
147
+
148
+ def _stop_all():
149
+ for session_id in list(_attendants):
150
+ _stop_attendant(session_id)
23
151
 
24
152
  def _activation(info):
25
153
  platform = str(info.get("platform") or "")
@@ -27,7 +155,9 @@ def _activation(info):
27
155
  return ""
28
156
  if os.environ.get("HERMES_SESSION_SOURCE", "").lower() not in ("", "cli"):
29
157
  return ""
30
- if os.environ.get("_HERMES_GATEWAY") == "1" or os.environ.get("HERMES_TUI_ACTIVE_SESSION_FILE"):
158
+ # Hermes may import gateway.run inside a classic CLI and set its process
159
+ # marker there. The attached CLI object and exact session are the proof.
160
+ if os.environ.get("HERMES_TUI_ACTIVE_SESSION_FILE"):
31
161
  return ""
32
162
  if os.environ.get("TERMINAL_ENV", "").lower() not in ("", "local"):
33
163
  return ""
@@ -57,11 +187,19 @@ def _activation(info):
57
187
  timeout=10, check=False)
58
188
  except (OSError, subprocess.TimeoutExpired):
59
189
  return ""
60
- return result.stdout.strip() if result.returncode == 0 else ""
190
+ content = result.stdout.strip() if result.returncode == 0 else ""
191
+ if content and not worker:
192
+ _start_attendant(_ctx, session_id, cwd)
193
+ return content
61
194
 
62
195
  def register(ctx):
196
+ global _ctx
197
+ _ctx = ctx
63
198
  ctx.register_system_prompt_section("notifai.activation", _activation,
64
199
  position="after_memory", max_chars=4000)
200
+ ctx.register_hook("on_session_finalize", lambda session_id=None, **_: _stop_attendant(str(session_id or "")))
201
+ ctx.register_hook("on_session_reset", lambda session_id=None, **_: _stop_all())
202
+ atexit.register(_stop_all)
65
203
  `;
66
204
  }
67
205
  export function isOurHermesPlugin(dir) {
@@ -121,7 +259,7 @@ export function installHermesPlugin(adapterPath, env = process.env, nodePath) {
121
259
  else {
122
260
  const temp = mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-plugin-'));
123
261
  try {
124
- writeFileSync(path.join(temp, 'plugin.yaml'), 'name: notifai\nversion: "1.0.0"\ndescription: "Notifai Project Enablement and agent guidance"\n');
262
+ writeFileSync(path.join(temp, 'plugin.yaml'), 'name: notifai\nversion: "1.0.0"\ndescription: "Notifai activation and Session Attendant"\nprovides_hooks:\n - on_session_finalize\n - on_session_reset\n');
125
263
  writeFileSync(path.join(temp, '__init__.py'), source);
126
264
  const git = (args) => {
127
265
  const result = spawnSync('git', args, { cwd: temp, env, encoding: 'utf8', timeout: 10_000 });
@@ -1 +1 @@
1
- {"version":3,"file":"hermes-plugin.js","sourceRoot":"","sources":["../src/hermes-plugin.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hermes-plugin.js","sourceRoot":"","sources":["../src/hermes-plugin.ts"],"names":[],"mappings":"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"}
@@ -65,7 +65,7 @@ export declare function reportHandedOffAfterTheFact(ctx: HookContext, sessionId:
65
65
  * hook for this session will ever run again. A question whose
66
66
  * agent just exited can receive no answer, so it is orphaned as `expired`.
67
67
  */
68
- export declare function handleSessionEnd(env: NodeJS.ProcessEnv, envelope: HookEnvelope, now?: number): HookOutcome;
68
+ export declare function handleSessionEnd(env: NodeJS.ProcessEnv, envelope: HookEnvelope, now?: number, preserveAccepted?: boolean): HookOutcome;
69
69
  /**
70
70
  * Registrations still waiting for their first push. More than this in one turn
71
71
  * means an agent is looping, not asking, and related questions belong in one
@@ -8,7 +8,7 @@ import { clearMatchingProjectSession, writeProjectSession } from './hook-project
8
8
  import { claimHandoffState, claimQuestionPush, releaseQuestionPush } from './hook-question-lock.js';
9
9
  import { closeQuietly, drainOrphanRetirements, drainRetirements, finalizeReplies, selectedForClaimedDelivery, orphanRetirements, pendingAnsweredByPrompt, retirePendings, retiringQuestion, } from './hook-question-retirement.js';
10
10
  import { admitQueuedQuestion, clearFrozenSubmission, dropPendingQuestion, isSamePending, pendingQuestions, questionSubmissionAttempted, queuedQuestionStillEligible, rememberQuestionState, sourceContextAtHookEvent, summarizeRequestIds, } from './hook-question-state.js';
11
- import { clearSessionState, markSessionEnded, pendingList, readSessionState, sessionHasEnded, sessionStatePath, updateSessionState, writeSessionState, writeSessionStateUnlocked, recordSessionNotified, } from './hook-session-state.js';
11
+ import { clearSessionState, markSessionEnded, pendingList, readSessionState, readSessionIncarnation, sessionHasEnded, sessionStatePath, updateSessionState, writeSessionState, writeSessionStateUnlocked, recordSessionNotified, } from './hook-session-state.js';
12
12
  import { currentProcessIdentity } from './process-identity.js';
13
13
  import { userPromptContextOutput } from './session-activation.js';
14
14
  import { answersAlreadyWritten, beginHandOff, recordUnclaimedHandOffs, recoverDeliveryJournal, sweepDeliveryJournals, } from './session-delivery.js';
@@ -313,7 +313,7 @@ function answerCloseDisposition(ctx, route) {
313
313
  // question, but an immediate reply can beat the first lease exchange. The
314
314
  // service retains the exact-session owner and retries after the lease is
315
315
  // ready, so preserve the server's selected delivery claim at close.
316
- if (ctx.harness === 'openclaw' && route.kind === 'session-queue')
316
+ if ((ctx.harness === 'openclaw' || ctx.harness === 'hermes') && route.kind === 'session-queue')
317
317
  return 'deliver';
318
318
  return claimableRoute(route) && ctx.answerClaims?.lease() != null ? 'deliver' : undefined;
319
319
  }
@@ -442,7 +442,9 @@ function withReminder(reminder, context) {
442
442
  * the service, or null when none is.
443
443
  */
444
444
  async function messageAcknowledgementReminder(ctx, sessionId) {
445
- const owed = presentedMessageAcknowledgements(readSessionState(sessionId, ctx.env));
445
+ const generation = ctx.harness === 'openclaw' ? readSessionIncarnation(sessionId, ctx.env)?.openclaw_generation : undefined;
446
+ const owed = presentedMessageAcknowledgements(readSessionState(sessionId, ctx.env))
447
+ .filter((entry) => ctx.harness !== 'openclaw' || (generation !== undefined && entry.openclaw_generation === generation));
446
448
  if (owed.length === 0)
447
449
  return null;
448
450
  const still = await reconcileAcknowledgementObligations(ctx, sessionId, owed);
@@ -982,9 +984,11 @@ async function deliverAcceptedAnswers(ctx, sessionId, route, accepted, notes, cw
982
984
  }
983
985
  await handOff?.finish(delivered.acknowledgement === 'delivered'
984
986
  ? 'written'
985
- : delivered.log?.['reason'] === WRITE_ABORTED_REASON
986
- ? 'aborted'
987
- : 'not-written');
987
+ : delivered.log?.['reason'] === 'write-unconfirmed'
988
+ ? 'failed'
989
+ : delivered.log?.['reason'] === WRITE_ABORTED_REASON
990
+ ? 'aborted'
991
+ : 'not-written');
988
992
  if (delivered.acknowledgement === 'delivered' && deliveryCommitted) {
989
993
  await reportUnclaimedHandOff(ctx, sessionId, answered, handOff);
990
994
  }
@@ -1178,8 +1182,13 @@ async function claimAnswerHandOff(ctx, sessionId, route, answered) {
1178
1182
  // attendant has had a moment to re-acquire; any other refusal is final.
1179
1183
  const fenced = handOff.claimed.length === 0 &&
1180
1184
  handOff.refused.every((refusal) => refusal.reason === 'generation_fenced');
1181
- if (!fenced || round >= ANSWER_CLAIM_ROUNDS)
1185
+ if (!fenced || round >= ANSWER_CLAIM_ROUNDS) {
1186
+ if (ctx.harness === 'hermes' && handOff.claimed.length !== subjects.length) {
1187
+ await handOff.finish('not-written');
1188
+ return null;
1189
+ }
1182
1190
  return handOff;
1191
+ }
1183
1192
  await handOff.finish('not-written');
1184
1193
  await ctx.sleep(ANSWER_CLAIM_RETRY_MS);
1185
1194
  }
@@ -1791,7 +1800,7 @@ async function escalate(ctx, envelope, sessionId, unasked, alreadyLive, notes, h
1791
1800
  * hook for this session will ever run again. A question whose
1792
1801
  * agent just exited can receive no answer, so it is orphaned as `expired`.
1793
1802
  */
1794
- export function handleSessionEnd(env, envelope, now = Date.now()) {
1803
+ export function handleSessionEnd(env, envelope, now = Date.now(), preserveAccepted = true) {
1795
1804
  const notes = [];
1796
1805
  const sessionId = envelope.session_id;
1797
1806
  if (!sessionId)
@@ -1840,10 +1849,12 @@ export function handleSessionEnd(env, envelope, now = Date.now()) {
1840
1849
  orphanRetirements(env, orphans, now);
1841
1850
  notes.push(`queued ${orphans.length} question${orphans.length > 1 ? 's' : ''} for retirement on the next hook`);
1842
1851
  }
1843
- if (state.accepted !== undefined ||
1852
+ if ((preserveAccepted && state.accepted !== undefined) ||
1844
1853
  (state.acknowledgement_due?.length ?? 0) > 0 ||
1845
1854
  (state.message_acknowledgement_due?.length ?? 0) > 0) {
1846
1855
  const preserved = { ...stateWithHistory };
1856
+ if (!preserveAccepted)
1857
+ delete preserved.accepted;
1847
1858
  delete preserved.pending;
1848
1859
  delete preserved.retiring;
1849
1860
  delete preserved.acknowledgement_blocks;
@@ -1854,13 +1865,13 @@ export function handleSessionEnd(env, envelope, now = Date.now()) {
1854
1865
  delete preserved.message_acknowledgement_due;
1855
1866
  }
1856
1867
  writeSessionState(sessionId, env, preserved);
1857
- notes.push(state.accepted !== undefined
1868
+ notes.push(preserveAccepted && state.accepted !== undefined
1858
1869
  ? 'preserved an accepted device answer for this exact session to resume'
1859
1870
  : 'preserved required Agent Acknowledgement obligations for this exact session');
1860
1871
  return {
1861
1872
  notes,
1862
1873
  log: {
1863
- outcome: state.accepted !== undefined ? 'answer-preserved' : 'acknowledgement-preserved',
1874
+ outcome: preserveAccepted && state.accepted !== undefined ? 'answer-preserved' : 'acknowledgement-preserved',
1864
1875
  queued_retirements: orphans.length,
1865
1876
  accepted_answers: state.accepted?.answers.length ?? 0,
1866
1877
  acknowledgement_due: state.acknowledgement_due?.length ?? 0,