omp-conductor 0.5.6 → 0.6.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.
- package/README.md +20 -0
- package/package.json +1 -1
- package/src/approval-surface.ts +90 -1
- package/src/briefs/orchestrator.md +14 -9
- package/src/briefs/policy.md +5 -4
- package/src/fleet.ts +19 -1
- package/src/orchestrator-tick.ts +76 -6
package/README.md
CHANGED
|
@@ -204,6 +204,26 @@ Also required on the host:
|
|
|
204
204
|
unconfigured rather than guessed at, on the grounds that a health row which
|
|
205
205
|
reads green over a broken contract is worse than one that overstates a fault.
|
|
206
206
|
|
|
207
|
+
- **A fleet that answers instead of narrating** needs one key, set once:
|
|
208
|
+
`/telegram set profile daemon` (omp-telegram 0.11.0 or newer). Without it the
|
|
209
|
+
bridge behaves as it does on a laptop: it finalizes a real Telegram message
|
|
210
|
+
per assistant turn for as long as a conversation is active — so one answer
|
|
211
|
+
arrives as several messages, and a message that lands mid-tick keeps relaying
|
|
212
|
+
that tick's internal turns — and it posts every local run's closing text to
|
|
213
|
+
`notifyChat`, which on a host whose runs are heartbeat ticks means each tick's
|
|
214
|
+
working prose. The profile switches all of it off at the transport: text
|
|
215
|
+
reaches Telegram only through `telegram_send` / `telegram_ask`, the idle post
|
|
216
|
+
is suppressed, and `telegram_ask` stays mounted and aimed at the paired owner
|
|
217
|
+
on every turn — including a locally injected tick, so it also removes the need
|
|
218
|
+
for `notifyMode` above. Approval and blocked-input pings still fire; those
|
|
219
|
+
mean a human is needed, which is the point of the channel.
|
|
220
|
+
|
|
221
|
+
`omp-conductor status` reports an interactive profile on the `telegram` row,
|
|
222
|
+
and every tick composed on one carries a prompt line saying so. Note the two
|
|
223
|
+
settings pull against each other before the profile exists: setting
|
|
224
|
+
`notifyMode` to make `telegram_ask` mountable is exactly what arms the idle
|
|
225
|
+
post, so the correctly askable fleet was also the loud one.
|
|
226
|
+
|
|
207
227
|
With neither, tier 2 degrades to a comment on the issue. Nothing is broken in
|
|
208
228
|
that configuration: it is supported, just slower to reach you.
|
|
209
229
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
package/src/approval-surface.ts
CHANGED
|
@@ -33,6 +33,17 @@
|
|
|
33
33
|
* The legacy `away: true` boolean counts: `loadAccess()` migrates it to
|
|
34
34
|
* `notifyMode: "away"` on read, so a fleet still carrying it resolves a target
|
|
35
35
|
* and must not be reported as broken.
|
|
36
|
+
*
|
|
37
|
+
* Since omp-telegram 0.11.0 there is a second way for a locally injected turn to
|
|
38
|
+
* resolve a target, and it counts here for the same reason the legacy boolean
|
|
39
|
+
* does: `profile: "daemon"` makes `notifyTarget()` resolve without `notifyMode`
|
|
40
|
+
* at all, because the profile *is* the headless contract — explicit-only
|
|
41
|
+
* outbound, no idle notify post, and `telegram_ask` targeted at the paired owner
|
|
42
|
+
* on every turn including a cron tick. A fleet carrying it has the surface #114
|
|
43
|
+
* was about, so demanding `notifyMode` beside it would be the false alarm this
|
|
44
|
+
* file exists to avoid. The `notifyMode` paths are all still live: an older
|
|
45
|
+
* plugin ignores the `profile` key entirely, and a host running one keeps
|
|
46
|
+
* working unchanged.
|
|
36
47
|
*/
|
|
37
48
|
|
|
38
49
|
import { readFileSync } from "node:fs";
|
|
@@ -46,6 +57,16 @@ export const TELEGRAM_APPROVAL_TOOL = "telegram_ask";
|
|
|
46
57
|
|
|
47
58
|
export type ApprovalSurface = { kind: "ready" } | { kind: "missing"; reason: string };
|
|
48
59
|
|
|
60
|
+
/** Whether the Telegram bridge is running the headless contract, or merely
|
|
61
|
+
* configured well enough to answer a question.
|
|
62
|
+
*
|
|
63
|
+
* Two questions, not one, which is why this is a separate answer from
|
|
64
|
+
* {@link ApprovalSurface}: a fleet can be perfectly *askable* and still be
|
|
65
|
+
* narrating every turn into its operator's chat. `interactive` carries the
|
|
66
|
+
* reason because both readers of it — a tick prompt and a status row — have to
|
|
67
|
+
* name the remedy, and one spelling of it keeps them in agreement. */
|
|
68
|
+
export type DaemonProfileSurface = { kind: "daemon" } | { kind: "interactive"; reason: string };
|
|
69
|
+
|
|
49
70
|
/** One checked read of a JSON property, so nothing below asserts a shape the
|
|
50
71
|
* parse never proved. Anything that is not a plain object, or a key that is
|
|
51
72
|
* absent, answers undefined — which every caller here already treats as
|
|
@@ -156,7 +177,20 @@ export function readApprovalSurface(path: string): ApprovalSurface {
|
|
|
156
177
|
};
|
|
157
178
|
}
|
|
158
179
|
const mode = field(access, "notifyMode");
|
|
159
|
-
|
|
180
|
+
// Three spellings of the same fact, and none of them is redundant.
|
|
181
|
+
// `notifyMode` is what an interactive host sets; `away: true` is the retired
|
|
182
|
+
// boolean `loadAccess()` migrates on read; `profile: "daemon"` is the headless
|
|
183
|
+
// contract, which omp-telegram ≥ 0.11.0 treats as its own reason to resolve a
|
|
184
|
+
// notify target (`notifyTarget()` gates on `notifyMode || profile === "daemon"`
|
|
185
|
+
// and `before_agent_start` mounts `telegram_ask` on the same disjunction). A
|
|
186
|
+
// daemon-profile fleet with no `notifyMode` is therefore *more* answerable than
|
|
187
|
+
// the configuration #114 asked for, not less, and reporting it broken would be
|
|
188
|
+
// the every-interval false alarm this file was written to prevent.
|
|
189
|
+
const active =
|
|
190
|
+
mode === "away" ||
|
|
191
|
+
mode === "always" ||
|
|
192
|
+
field(access, "away") === true ||
|
|
193
|
+
field(access, "profile") === "daemon";
|
|
160
194
|
if (!active) {
|
|
161
195
|
return {
|
|
162
196
|
kind: "missing",
|
|
@@ -251,3 +285,58 @@ export function readApprovalSurface(path: string): ApprovalSurface {
|
|
|
251
285
|
}
|
|
252
286
|
return { kind: "ready" };
|
|
253
287
|
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Reads the same access file and answers the other question: does visible
|
|
291
|
+
* assistant text stay out of the operator's chat?
|
|
292
|
+
*
|
|
293
|
+
* A separate read rather than a field on {@link ApprovalSurface}, because the
|
|
294
|
+
* two faults have different remedies and a caller may care about only one. This
|
|
295
|
+
* one is about *noise*: without the profile, omp-telegram relays a real Telegram
|
|
296
|
+
* message per assistant turn for as long as a conversation is marked active
|
|
297
|
+
* (`outbound.ts` `onTurnEnd`), and — because the fleet sets `notifyMode` for the
|
|
298
|
+
* approval surface above — posts every local run's closing text to the notify
|
|
299
|
+
* chat from its `agent_end` handler. So a correctly *askable* fleet is exactly
|
|
300
|
+
* the one that narrates: the two settings pull against each other, which is why
|
|
301
|
+
* both are checked and only the profile silences the second.
|
|
302
|
+
*
|
|
303
|
+
* One key decides it, deliberately. `profile: "daemon"` is omp-telegram ≥ 0.11.0's
|
|
304
|
+
* whole headless contract — explicit-only outbound, no idle/final notify post,
|
|
305
|
+
* `telegram_ask` targeted at the paired owner on every turn — so verifying it is
|
|
306
|
+
* one comparison rather than three inferences about `streaming`, `notifyMode` and
|
|
307
|
+
* turn origin that would drift the moment the plugin changes. Anything other
|
|
308
|
+
* than the literal string is `interactive`: `loadAccess()` drops every other
|
|
309
|
+
* value to undefined, so `"DAEMON"` and `5` are not the contract on the host
|
|
310
|
+
* either, and reporting them as one would promise a suppression nothing applied.
|
|
311
|
+
*
|
|
312
|
+
* Never throws, and never writes: conductor reads this file, the operator's
|
|
313
|
+
* `/telegram set profile daemon` is what changes it. An unreadable or corrupt
|
|
314
|
+
* file answers `interactive` for the reason the whole module fails closed — a
|
|
315
|
+
* green signal over an unproven contract is what #114 was.
|
|
316
|
+
*/
|
|
317
|
+
export function readDaemonProfile(accessPath: string): DaemonProfileSurface {
|
|
318
|
+
const remedy = "assistant text can auto-relay to Telegram — run `/telegram set profile daemon`";
|
|
319
|
+
let access: unknown;
|
|
320
|
+
try {
|
|
321
|
+
access = JSON.parse(readFileSync(accessPath, "utf8"));
|
|
322
|
+
} catch {
|
|
323
|
+
return {
|
|
324
|
+
kind: "interactive",
|
|
325
|
+
reason: `cannot read or parse ${accessPath}, so no telegram profile is proven; ${remedy}`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (access === null || typeof access !== "object" || Array.isArray(access)) {
|
|
329
|
+
return {
|
|
330
|
+
kind: "interactive",
|
|
331
|
+
reason: `${accessPath} is not an object, so no telegram profile is proven; ${remedy}`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const profile = field(access, "profile");
|
|
335
|
+
if (profile === "daemon") return { kind: "daemon" };
|
|
336
|
+
// An absent key reads as "default" rather than "undefined": that is the word
|
|
337
|
+
// the plugin's own `/telegram status` line and its `profile: daemon | default`
|
|
338
|
+
// help text use, and an operator matching a status row against this reason
|
|
339
|
+
// should not have to translate.
|
|
340
|
+
const observed = profile === undefined ? "default" : JSON.stringify(profile);
|
|
341
|
+
return { kind: "interactive", reason: `telegram profile is ${observed}; ${remedy}` };
|
|
342
|
+
}
|
|
@@ -208,18 +208,23 @@ sent that way is undetectable when it does not arrive.
|
|
|
208
208
|
|
|
209
209
|
## Human messages
|
|
210
210
|
|
|
211
|
-
A human writing to you between ticks is not a tick. Answer
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
A human writing to you between ticks is not a tick. Answer with a **single
|
|
212
|
+
`telegram_send` call** — one message, the answer only, from evidence you already
|
|
213
|
+
hold or go and fetch. Never answer as plain end-of-turn text: on this session,
|
|
214
|
+
text you merely write reaches nobody — if you do not call `telegram_send`, the
|
|
215
|
+
person gets silence. While handling any turn, produce no visible commentary
|
|
216
|
+
between tool calls — reasoning stays in thinking, actions stay in tools.
|
|
217
|
+
|
|
218
|
+
If the answer needs a decision from the operator (a choice, a yes/no, an
|
|
219
|
+
approval), ask it with `telegram_ask` — never a numbered-options message via
|
|
220
|
+
`telegram_send`, and never the generic `ask` UI. A cancelled or errored
|
|
221
|
+
`telegram_ask` is a delivery failure, not an answer.
|
|
216
222
|
|
|
217
223
|
A message may also reach you **mid-tick** (delivery is steering: it arrives
|
|
218
224
|
between two of your tool calls). Treat it as an interrupt, not a new tick:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
person is waiting now.
|
|
225
|
+
`telegram_send` the answer immediately, then return to the duty you were in the
|
|
226
|
+
middle of and finish it. Never abandon or restart the tick because a message
|
|
227
|
+
arrived, and never batch the answer "for the report" — the person is waiting now.
|
|
223
228
|
|
|
224
229
|
## Escalation tiers
|
|
225
230
|
|
package/src/briefs/policy.md
CHANGED
|
@@ -74,10 +74,11 @@ Your report scope is **`{{REPORT_SCOPE}}`**. All three scopes, spelled out:
|
|
|
74
74
|
issue you pulled off the queue, a cap that stopped the fleet. A tick where
|
|
75
75
|
nothing changed still says nothing — "no change" is not an event.
|
|
76
76
|
|
|
77
|
-
**Delivery.**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
**Delivery.** Never rely on end-of-turn text reaching anyone. The only delivery
|
|
78
|
+
paths are `omp-conductor report` (reports — persisted, daemon-retried),
|
|
79
|
+
`telegram_send` (a person who is waiting), and `telegram_ask` (a decision).
|
|
80
|
+
Everything else is noise or silence. Hand every reportable event to the
|
|
81
|
+
conductor's outbox:
|
|
81
82
|
|
|
82
83
|
```
|
|
83
84
|
omp-conductor report --text "<the whole report>" # a material event
|
package/src/fleet.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { homedir } from "node:os";
|
|
|
29
29
|
import { dirname, join } from "node:path";
|
|
30
30
|
import { findProject, loadConfig, stateDir } from "./config.ts";
|
|
31
31
|
import { planUsageLine, readPlanUsage, sharedUsageSource } from "./usage.ts";
|
|
32
|
-
import { readApprovalSurface } from "./approval-surface.ts";
|
|
32
|
+
import { readApprovalSurface, readDaemonProfile } from "./approval-surface.ts";
|
|
33
33
|
import { inspectBriefLayout } from "./brief-upgrade.ts";
|
|
34
34
|
import { dbPath, openStore } from "./store.ts";
|
|
35
35
|
import { renderBriefForProject } from "./setup.ts";
|
|
@@ -1303,6 +1303,24 @@ export async function probeTelegramHealth(
|
|
|
1303
1303
|
if (approval.kind === "missing") {
|
|
1304
1304
|
return { kind: "degraded", detail: `${username}; inbound configured; ${approval.reason}` };
|
|
1305
1305
|
}
|
|
1306
|
+
// Last, and only once the surface is answerable: whether it is *quiet*.
|
|
1307
|
+
//
|
|
1308
|
+
// The order is the order an operator should fix things in, and one row carries
|
|
1309
|
+
// one remedy. A missing token means nothing outbound works, so the approval
|
|
1310
|
+
// surface is not worth discussing; a missing approval surface means an
|
|
1311
|
+
// amendment cannot be asked, which outranks noise; an interactive profile is
|
|
1312
|
+
// real but strictly the least severe — the fleet works, it is just loud. And
|
|
1313
|
+
// the two lower checks are not independent of each other: setting `notifyMode`
|
|
1314
|
+
// to fix the approval surface is what *arms* the `agent_end` notify post this
|
|
1315
|
+
// one warns about, so a fleet that fixes them in the other order would see the
|
|
1316
|
+
// noise appear as the reward for fixing the silence.
|
|
1317
|
+
const profile = readDaemonProfile(accessPath);
|
|
1318
|
+
if (profile.kind === "interactive") {
|
|
1319
|
+
return {
|
|
1320
|
+
kind: "degraded",
|
|
1321
|
+
detail: `${username}; inbound configured; interactive profile relays assistant text — /telegram set profile daemon`,
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1306
1324
|
return { kind: "ok", detail: `${username}; inbound configured; telegram_ask available` };
|
|
1307
1325
|
}
|
|
1308
1326
|
|
package/src/orchestrator-tick.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
bridgeTokenBound,
|
|
53
53
|
hasBotToken,
|
|
54
54
|
readApprovalSurface,
|
|
55
|
+
readDaemonProfile,
|
|
55
56
|
TELEGRAM_APPROVAL_TOOL,
|
|
56
57
|
} from "./approval-surface.ts";
|
|
57
58
|
import {
|
|
@@ -363,12 +364,27 @@ export const TICK_SCOPE_CONSTRAINTS: { readonly [K in ReportScope]: string } = {
|
|
|
363
364
|
/**
|
|
364
365
|
* The delivery clause, appended to every default tick prompt.
|
|
365
366
|
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
367
|
+
* A tick's end-of-turn text does not reach the operator as a *report*, and the
|
|
368
|
+
* rule's job is to stop a session believing otherwise: on 2026-08-06 the fleet
|
|
369
|
+
* this extension runs produced a release report and two tier-2 escalations as
|
|
370
|
+
* end-of-turn text, and not one of the three reached anybody.
|
|
371
|
+
*
|
|
372
|
+
* This comment used to explain that by saying end-of-turn text streams to
|
|
373
|
+
* Telegram only on a turn that *began* as an inbound Telegram message. That
|
|
374
|
+
* premise is false, and the correction matters because it flips the reason:
|
|
375
|
+
* omp-telegram's `agent_end` handler posts `finalText` to the notify chat on
|
|
376
|
+
* every run that did *not* come from Telegram, whenever `notifyMode` is set —
|
|
377
|
+
* and the fleet must set it, because {@link readApprovalSurface} needs it for
|
|
378
|
+
* the approval surface. So a tick's closing prose *was* reaching the operator
|
|
379
|
+
* all along: not as a delivered report, as an unlogged, unretried, untracked
|
|
380
|
+
* ping in the middle of whatever else was in that chat. Both the void and the
|
|
381
|
+
* ping are the same fault seen from two sides — text is not a delivery channel.
|
|
382
|
+
*
|
|
383
|
+
* omp-telegram 0.11.0's `profile: "daemon"` removes it in the other direction:
|
|
384
|
+
* explicit-only outbound, and the idle/final notify post suppressed outright, so
|
|
385
|
+
* a tick's visible text goes nowhere at all. That is the configuration the fleet
|
|
386
|
+
* runs (see {@link TICK_NARRATION_RULE}, which warns when it is absent), and it
|
|
387
|
+
* makes this rule's demand literal rather than merely prudent.
|
|
372
388
|
*
|
|
373
389
|
* The clause used to name `telegram_send`, and that held — but it was still an
|
|
374
390
|
* instruction where a mechanism was needed: the same miss recurs whenever the
|
|
@@ -418,6 +434,38 @@ export const TICK_APPROVAL_UNAVAILABLE_RULE =
|
|
|
418
434
|
`If you have an amendment to propose, deliver the question with telegram_send and wait for your operator's reply on a later turn; ` +
|
|
419
435
|
`never apply an amendment, or record one as approved, without an explicit answer you actually received.`;
|
|
420
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Appended to every tick — the shipped prompt or the operator's own — composed
|
|
439
|
+
* on a bridge that is not in omp-telegram's `profile: "daemon"`.
|
|
440
|
+
*
|
|
441
|
+
* The running commentary an operator sees is *mechanical*, not disobedience, and
|
|
442
|
+
* that is why a prompt line is the wrong permanent fix and the right interim one.
|
|
443
|
+
* Two verified paths carry visible text out without anyone calling a tool:
|
|
444
|
+
* `outbound.ts` `onTurnEnd()` finalizes one real Telegram message per assistant
|
|
445
|
+
* turn for as long as the chat is marked active — so a multi-step answer arrives
|
|
446
|
+
* as several messages, and a message that lands mid-tick keeps the chat active
|
|
447
|
+
* for the rest of that run, relaying every subsequent tick-internal turn — and
|
|
448
|
+
* the `agent_end` handler posts the closing text of every *local* run to the
|
|
449
|
+
* notify chat whenever `notifyMode` is set. The fleet has to set `notifyMode`
|
|
450
|
+
* (see {@link TICK_APPROVAL_UNAVAILABLE_RULE}), so the correctly-askable fleet is
|
|
451
|
+
* exactly the narrating one. `profile: "daemon"` closes both at the transport:
|
|
452
|
+
* explicit-only outbound, notify post suppressed.
|
|
453
|
+
*
|
|
454
|
+
* So this rule is a stopgap with an expiry date, and it says so: it names the
|
|
455
|
+
* one command that removes it. Until then the only defence is discipline the
|
|
456
|
+
* turn can actually exercise — no visible text between tool calls, and one
|
|
457
|
+
* `telegram_send` for a human answer rather than prose that leaks a turn at a
|
|
458
|
+
* time. It fires *because* the mechanism is absent, so a fleet that has run
|
|
459
|
+
* `/telegram set profile daemon` never sees it.
|
|
460
|
+
*
|
|
461
|
+
* Appended to a configured `message` too, for {@link TICK_APPROVAL_UNAVAILABLE_RULE}'s
|
|
462
|
+
* reason exactly: an operator's prompt owns the reporting contract, but it cannot
|
|
463
|
+
* consent on the orchestrator's behalf to what the transport does with its text.
|
|
464
|
+
* Transport truth is not the operator's prompt to waive.
|
|
465
|
+
*/
|
|
466
|
+
export const TICK_NARRATION_RULE =
|
|
467
|
+
"This session's Telegram bridge is NOT in daemon profile, so visible assistant text can auto-relay to your operator's chat (per-turn messages while a Telegram conversation is active, and end-of-run notify posts on local runs). Until the operator runs `/telegram set profile daemon`: produce no visible text between tool calls, and answer any human message with a single telegram_send call only.";
|
|
468
|
+
|
|
421
469
|
function frictionLabel(kind: FrictionSignal["kind"]): string {
|
|
422
470
|
if (kind.startsWith("admission:")) return `admission hold ${kind.slice("admission:".length)}`;
|
|
423
471
|
if (kind === "feedback:escalation-should-digest") return "escalations classified as digest material";
|
|
@@ -1254,6 +1302,28 @@ function tick(pi: TickApi, ctx: TickContext, config: TickConfig, session: TickSe
|
|
|
1254
1302
|
}
|
|
1255
1303
|
}
|
|
1256
1304
|
|
|
1305
|
+
// The other half of transport truth, read from the same file at the same
|
|
1306
|
+
// moment: whether visible text stays out of the operator's chat. Appended
|
|
1307
|
+
// after the approval line because it is the weaker instruction of the two —
|
|
1308
|
+
// the approval rule stops an invented approval, this one asks the turn to keep
|
|
1309
|
+
// quiet — and a prompt should end on the sentence that must not be missed.
|
|
1310
|
+
//
|
|
1311
|
+
// Gated on `bridgeTokenAtStart` for the same reason the approval read is, and
|
|
1312
|
+
// the reasoning is the mirror image of it: with no token bound, omp-telegram
|
|
1313
|
+
// sends *nothing*, so the two relay paths this rule warns about cannot fire
|
|
1314
|
+
// either. A narration warning on a dead bridge would describe a hazard that
|
|
1315
|
+
// does not exist, on top of an approval warning that already names the one
|
|
1316
|
+
// command worth running (`/telegram on`) — two remedies on one prompt, and the
|
|
1317
|
+
// operator acts on neither. So the rule fires only where the leak is real: a
|
|
1318
|
+
// live bridge whose profile is not `daemon`.
|
|
1319
|
+
//
|
|
1320
|
+
// No `accessFile` means no fleet bridge to judge, exactly as above.
|
|
1321
|
+
const profile =
|
|
1322
|
+
config.accessFile === undefined || !session.bridgeTokenAtStart
|
|
1323
|
+
? undefined
|
|
1324
|
+
: readDaemonProfile(config.accessFile);
|
|
1325
|
+
if (profile?.kind === "interactive") content = `${content}\n${TICK_NARRATION_RULE}`;
|
|
1326
|
+
|
|
1257
1327
|
try {
|
|
1258
1328
|
pi.sendMessage(
|
|
1259
1329
|
{ customType: TICK_CUSTOM_TYPE, content, display: true, attribution: "user" },
|