claude-threads 1.37.1 → 1.38.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/CHANGELOG.md +14 -2
- package/dist/index.js +95 -16
- package/docs/CONFIGURATION.md +52 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,12 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [1.38.0] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`mode: assistant` names the replies-only shape** (#590, building on #505). A platform entry already carried three fields deciding how much of the bot's scaffolding a channel sees (`sessionHeader`, `stickyMessage`, `lifecycle`), and two deployments described the same combination of all three in nearly the same words. `mode` names it: `full` (default) is the current behaviour, `assistant` hides all three so Claude's reply is the whole thread. A field set explicitly still wins, so `mode: assistant` with `sessionHeader: full` means "replies only, but keep the header". The preset expands when the config loads, so everything downstream reads the same concrete values as before and omitting `mode` behaves identically to today. `turnMarker` stays outside the preset: it is a signal for integrations reading the channel, not something a human sees. Startup warns when a preset is countermanded by an explicit field, because the setup wizard writes `sessionHeader` and `stickyMessage` whenever your answer differed from the default: adding `mode: assistant` to a wizard-made config would otherwise move `lifecycle` alone while looking like it moved everything.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **A failed auto-update no longer retries every second forever** (#601). When the first scheduling check already met the restart condition (quiet or scheduled mode with idle sessions, ask mode with no threads), the countdown started before the 10-second check interval existed, so that interval survived and started a second countdown. The first countdown's interval was orphaned: at zero it cleared the new one instead of itself and kept firing "ready" once a second. A successful install exits the process, so this only showed when the install failed, as two posts per second (installing, failed) for as long as the bot ran. The check interval is now armed before the first check, a running countdown is never restarted, and a countdown clears its own interval at zero.
|
|
15
|
+
- **Deferring, cancelling or forcing an update now controls a running countdown** (#601). `!update defer` during the 60-second countdown left the countdown running, so the update installed anyway, and afterwards nothing re-armed the scheduler: a one-hour deferral silently became "until the next release". A deferral now stops the countdown, holds off every trigger (including late ask votes) for the window, and resumes checking when the window ends, asking again in ask mode. `!update now` during a countdown no longer installs a second time when the countdown reaches zero, and cancelling the schedule stops the countdown too.
|
|
16
|
+
|
|
17
|
+
## [1.37.2] - 2026-09-18
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Claude CLI 2.1.276 is verified.** Anyone running a CLI between 2.1.264 and 2.1.276 loses the "⚠️ untested" marker in the sticky message and session headers. Every capture except `compact-failed` was recorded fresh against it: no event type the bot consumes changed and no new type appeared, so the mock needed no adjustment. `compact-failed` deliberately keeps its older recording, because an immediate `/compact` on a fresh session now succeeds instead of failing, and the failure shape it pins is still live code.
|
|
9
21
|
|
|
10
22
|
## [1.37.1] - 2026-09-18
|
|
11
23
|
|
|
12
24
|
Dependency maintenance only. No source changes, so nothing about how the bot
|
|
13
|
-
behaves is different
|
|
25
|
+
behaves is different, but `bun build --target node` bundles dependencies into
|
|
14
26
|
`dist/`, so the versions below are the ones users actually run and they only
|
|
15
27
|
reach anyone through a release.
|
|
16
28
|
|
package/dist/index.js
CHANGED
|
@@ -2232,6 +2232,35 @@ function resolveOverheadVisibility(value, fieldPath) {
|
|
|
2232
2232
|
return value;
|
|
2233
2233
|
throw new Error(`Invalid ${fieldPath}: expected one of ${OVERHEAD_VISIBILITY_VALUES.join(", ")}, got ${JSON.stringify(value)}`);
|
|
2234
2234
|
}
|
|
2235
|
+
function isPresentationMode(value) {
|
|
2236
|
+
return typeof value === "string" && PRESENTATION_MODE_VALUES.includes(value);
|
|
2237
|
+
}
|
|
2238
|
+
function resolvePresentationMode(value, fieldPath) {
|
|
2239
|
+
if (value === undefined || value === null)
|
|
2240
|
+
return "full";
|
|
2241
|
+
if (isPresentationMode(value))
|
|
2242
|
+
return value;
|
|
2243
|
+
throw new Error(`Invalid ${fieldPath}: expected one of ${PRESENTATION_MODE_VALUES.join(", ")}, got ${JSON.stringify(value)}`);
|
|
2244
|
+
}
|
|
2245
|
+
function resolvePresentationOverhead(entry, fieldPath) {
|
|
2246
|
+
const preset = PRESENTATION_MODE_EXPANSIONS[resolvePresentationMode(entry.mode, `${fieldPath}.mode`)];
|
|
2247
|
+
const override = (value, field, fallback) => value === undefined || value === null ? fallback : resolveOverheadVisibility(value, `${fieldPath}.${field}`);
|
|
2248
|
+
return {
|
|
2249
|
+
sessionHeader: override(entry.sessionHeader, "sessionHeader", preset.sessionHeader),
|
|
2250
|
+
stickyMessage: override(entry.stickyMessage, "stickyMessage", preset.stickyMessage),
|
|
2251
|
+
lifecycle: override(entry.lifecycle, "lifecycle", preset.lifecycle)
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
function presentationOverridesAgainstPreset(entry) {
|
|
2255
|
+
if (!isPresentationMode(entry.mode) || entry.mode === "full")
|
|
2256
|
+
return [];
|
|
2257
|
+
const preset = PRESENTATION_MODE_EXPANSIONS[entry.mode];
|
|
2258
|
+
const fields = ["sessionHeader", "stickyMessage", "lifecycle"];
|
|
2259
|
+
return fields.filter((field) => {
|
|
2260
|
+
const value = entry[field];
|
|
2261
|
+
return value !== undefined && value !== null && value !== preset[field];
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2235
2264
|
function resolveTurnMarker(mode, emoji, platformType, fieldPath) {
|
|
2236
2265
|
const m = mode === undefined || mode === null ? "off" : mode;
|
|
2237
2266
|
if (!TURN_MARKER_VALUES.includes(m)) {
|
|
@@ -2423,10 +2452,19 @@ function effectivePermissionMode(input) {
|
|
|
2423
2452
|
return "default";
|
|
2424
2453
|
return input.botWideMode;
|
|
2425
2454
|
}
|
|
2426
|
-
var OVERHEAD_VISIBILITY_VALUES, DEFAULT_OVERHEAD_VISIBILITY = "full", RECONNECT_POLICY_VALUES, DEFAULT_RECONNECT_POLICY = "retry", TURN_MARKER_VALUES, DEFAULT_TURN_MARKER, DEFAULT_TURN_MARKER_EMOJI = "checkered_flag", TURN_COMPLETE_EVENT_TYPE = "claude_threads_turn_complete", TURN_COMPLETE_PAYLOAD_VERSION = 1, DEFAULT_MEMORY_CONFIG, MEMORY_DISABLED, BOT_MCP_SERVER_NAME = "claude-threads-mcp", STDIO_KEYS, REMOTE_KEYS, LIMITS_DEFAULTS, MODE_INFO;
|
|
2455
|
+
var OVERHEAD_VISIBILITY_VALUES, DEFAULT_OVERHEAD_VISIBILITY = "full", RECONNECT_POLICY_VALUES, DEFAULT_RECONNECT_POLICY = "retry", PRESENTATION_MODE_VALUES, PRESENTATION_MODE_EXPANSIONS, TURN_MARKER_VALUES, DEFAULT_TURN_MARKER, DEFAULT_TURN_MARKER_EMOJI = "checkered_flag", TURN_COMPLETE_EVENT_TYPE = "claude_threads_turn_complete", TURN_COMPLETE_PAYLOAD_VERSION = 1, DEFAULT_MEMORY_CONFIG, MEMORY_DISABLED, BOT_MCP_SERVER_NAME = "claude-threads-mcp", STDIO_KEYS, REMOTE_KEYS, LIMITS_DEFAULTS, MODE_INFO;
|
|
2427
2456
|
var init_types = __esm(() => {
|
|
2428
2457
|
OVERHEAD_VISIBILITY_VALUES = ["full", "minimal", "hidden"];
|
|
2429
2458
|
RECONNECT_POLICY_VALUES = ["retry", "exit"];
|
|
2459
|
+
PRESENTATION_MODE_VALUES = ["full", "assistant"];
|
|
2460
|
+
PRESENTATION_MODE_EXPANSIONS = {
|
|
2461
|
+
full: {
|
|
2462
|
+
sessionHeader: DEFAULT_OVERHEAD_VISIBILITY,
|
|
2463
|
+
stickyMessage: DEFAULT_OVERHEAD_VISIBILITY,
|
|
2464
|
+
lifecycle: DEFAULT_OVERHEAD_VISIBILITY
|
|
2465
|
+
},
|
|
2466
|
+
assistant: { sessionHeader: "hidden", stickyMessage: "hidden", lifecycle: "hidden" }
|
|
2467
|
+
};
|
|
2430
2468
|
TURN_MARKER_VALUES = ["reaction", "metadata", "off"];
|
|
2431
2469
|
DEFAULT_TURN_MARKER = { mode: "off" };
|
|
2432
2470
|
DEFAULT_MEMORY_CONFIG = {
|
|
@@ -4501,7 +4539,7 @@ function validateClaudeCli() {
|
|
|
4501
4539
|
rawOutput: result.rawOutput ?? undefined
|
|
4502
4540
|
};
|
|
4503
4541
|
}
|
|
4504
|
-
var import_semver, COMMON_CLAUDE_PATHS, CLAUDE_CLI_MIN_VERSION = "2.0.74", CLAUDE_CLI_VERIFIED_RANGE = ">=2.0.74 <2.2.0", CLAUDE_CLI_SUPPORTED_MAJOR = 2, CLAUDE_CLI_LATEST_VERIFIED = "2.1.
|
|
4542
|
+
var import_semver, COMMON_CLAUDE_PATHS, CLAUDE_CLI_MIN_VERSION = "2.0.74", CLAUDE_CLI_VERIFIED_RANGE = ">=2.0.74 <2.2.0", CLAUDE_CLI_SUPPORTED_MAJOR = 2, CLAUDE_CLI_LATEST_VERIFIED = "2.1.276", discoveredClaudePath = null;
|
|
4505
4543
|
var init_version_check = __esm(() => {
|
|
4506
4544
|
import_semver = __toESM(require_semver2(), 1);
|
|
4507
4545
|
COMMON_CLAUDE_PATHS = process.platform === "win32" ? [
|
|
@@ -72234,10 +72272,15 @@ async function handleReaction(deps, platformId, postId, emojiName, username, act
|
|
|
72234
72272
|
}
|
|
72235
72273
|
let session = deps.registry.findByPost(postId);
|
|
72236
72274
|
if (!session) {
|
|
72275
|
+
const hadInFlight = deps.registry.hasInFlightInteractivePost();
|
|
72237
72276
|
await deps.registry.awaitPendingPost(postId, UNKNOWN_POST_GRACE_MS);
|
|
72238
72277
|
session = deps.registry.findByPost(postId);
|
|
72239
|
-
if (!session)
|
|
72278
|
+
if (!session) {
|
|
72279
|
+
if (hadInFlight) {
|
|
72280
|
+
log52.debug(`Reaction :${normalizedEmoji}: by @${username} on unknown post ${postId} ` + `dropped after the in-flight grace window (${UNKNOWN_POST_GRACE_MS}ms)`);
|
|
72281
|
+
}
|
|
72240
72282
|
return;
|
|
72283
|
+
}
|
|
72241
72284
|
}
|
|
72242
72285
|
if (session.platformId !== platformId)
|
|
72243
72286
|
return;
|
|
@@ -85754,6 +85797,7 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
85754
85797
|
pendingUpdate = null;
|
|
85755
85798
|
checkTimer = null;
|
|
85756
85799
|
countdownTimer = null;
|
|
85800
|
+
deferTimer = null;
|
|
85757
85801
|
idleStartTime = null;
|
|
85758
85802
|
scheduledRestartAt = null;
|
|
85759
85803
|
askApprovals = new Map;
|
|
@@ -85776,6 +85820,8 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
85776
85820
|
}
|
|
85777
85821
|
cancelSchedule() {
|
|
85778
85822
|
this.stopChecking();
|
|
85823
|
+
this.stopCountdown();
|
|
85824
|
+
this.stopDeferTimer();
|
|
85779
85825
|
this.pendingUpdate = null;
|
|
85780
85826
|
this.idleStartTime = null;
|
|
85781
85827
|
this.scheduledRestartAt = null;
|
|
@@ -85784,9 +85830,21 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
85784
85830
|
log55.debug("Update schedule cancelled");
|
|
85785
85831
|
}
|
|
85786
85832
|
deferUpdate(minutes) {
|
|
85787
|
-
const
|
|
85833
|
+
const deferMs = minutes * 60 * 1000;
|
|
85834
|
+
const deferUntil = new Date(Date.now() + deferMs);
|
|
85835
|
+
this.stopCountdown();
|
|
85836
|
+
this.stopChecking();
|
|
85837
|
+
this.stopDeferTimer();
|
|
85788
85838
|
this.scheduledRestartAt = null;
|
|
85789
85839
|
this.idleStartTime = null;
|
|
85840
|
+
this.askApprovals.clear();
|
|
85841
|
+
this.askStartTime = null;
|
|
85842
|
+
if (this.pendingUpdate) {
|
|
85843
|
+
this.deferTimer = setTimeout(() => {
|
|
85844
|
+
this.deferTimer = null;
|
|
85845
|
+
this.startChecking();
|
|
85846
|
+
}, deferMs);
|
|
85847
|
+
}
|
|
85790
85848
|
this.emit("deferred", deferUntil);
|
|
85791
85849
|
log55.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
|
|
85792
85850
|
return deferUntil;
|
|
@@ -85808,12 +85866,13 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
85808
85866
|
stop() {
|
|
85809
85867
|
this.stopChecking();
|
|
85810
85868
|
this.stopCountdown();
|
|
85869
|
+
this.stopDeferTimer();
|
|
85811
85870
|
}
|
|
85812
85871
|
startChecking() {
|
|
85813
85872
|
if (this.checkTimer)
|
|
85814
85873
|
return;
|
|
85815
|
-
this.checkCondition();
|
|
85816
85874
|
this.checkTimer = setInterval(() => this.checkCondition(), 1e4);
|
|
85875
|
+
this.checkCondition();
|
|
85817
85876
|
log55.debug(`Started checking for ${this.config.autoRestartMode} condition`);
|
|
85818
85877
|
}
|
|
85819
85878
|
stopChecking() {
|
|
@@ -85943,23 +86002,35 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
85943
86002
|
if (!this.pendingUpdate)
|
|
85944
86003
|
return;
|
|
85945
86004
|
this.stopChecking();
|
|
86005
|
+
if (this.countdownTimer || this.deferTimer)
|
|
86006
|
+
return;
|
|
85946
86007
|
this.scheduledRestartAt = new Date(Date.now() + 60000);
|
|
85947
86008
|
let secondsRemaining = 60;
|
|
85948
86009
|
this.emit("countdown", secondsRemaining);
|
|
85949
|
-
|
|
86010
|
+
const timer = setInterval(() => {
|
|
85950
86011
|
secondsRemaining--;
|
|
85951
86012
|
this.emit("countdown", secondsRemaining);
|
|
85952
86013
|
if (secondsRemaining <= 0) {
|
|
85953
|
-
|
|
86014
|
+
clearInterval(timer);
|
|
86015
|
+
if (this.countdownTimer === timer)
|
|
86016
|
+
this.countdownTimer = null;
|
|
85954
86017
|
this.emit("ready", this.pendingUpdate);
|
|
85955
86018
|
}
|
|
85956
86019
|
}, 1000);
|
|
86020
|
+
this.countdownTimer = timer;
|
|
85957
86021
|
log55.info("Update countdown started (60 seconds)");
|
|
85958
86022
|
}
|
|
85959
86023
|
stopCountdown() {
|
|
85960
86024
|
if (this.countdownTimer) {
|
|
85961
86025
|
clearInterval(this.countdownTimer);
|
|
85962
86026
|
this.countdownTimer = null;
|
|
86027
|
+
this.scheduledRestartAt = null;
|
|
86028
|
+
}
|
|
86029
|
+
}
|
|
86030
|
+
stopDeferTimer() {
|
|
86031
|
+
if (this.deferTimer) {
|
|
86032
|
+
clearTimeout(this.deferTimer);
|
|
86033
|
+
this.deferTimer = null;
|
|
85963
86034
|
}
|
|
85964
86035
|
}
|
|
85965
86036
|
}
|
|
@@ -86346,6 +86417,7 @@ class AutoUpdateManager extends EventEmitter9 {
|
|
|
86346
86417
|
return;
|
|
86347
86418
|
}
|
|
86348
86419
|
log58.info("Forcing immediate update");
|
|
86420
|
+
this.scheduler.stop();
|
|
86349
86421
|
await this.performUpdate(updateInfo);
|
|
86350
86422
|
}
|
|
86351
86423
|
deferUpdate(minutes = 60) {
|
|
@@ -86731,6 +86803,15 @@ async function startWithoutDaemon() {
|
|
|
86731
86803
|
if (cliArgs.keepAlive !== undefined) {
|
|
86732
86804
|
newConfig.keepAlive = cliArgs.keepAlive;
|
|
86733
86805
|
}
|
|
86806
|
+
const presetOverrideWarnings = newConfig.platforms.flatMap((platformConfig) => {
|
|
86807
|
+
const overridden = presentationOverridesAgainstPreset(platformConfig);
|
|
86808
|
+
if (overridden.length === 0)
|
|
86809
|
+
return [];
|
|
86810
|
+
const one = overridden.length === 1;
|
|
86811
|
+
return [
|
|
86812
|
+
` ⚠️ platforms[${platformConfig.id}]: mode: ${String(platformConfig.mode)} is overridden for ${overridden.join(", ")} by ${one ? "an explicit field" : "explicit fields"}. Remove ${one ? "it" : "them"} to let the preset apply.`
|
|
86813
|
+
];
|
|
86814
|
+
});
|
|
86734
86815
|
if (cliArgs.sessionHeader !== undefined) {
|
|
86735
86816
|
for (const p of newConfig.platforms) {
|
|
86736
86817
|
p.sessionHeader = cliArgs.sessionHeader;
|
|
@@ -86745,14 +86826,15 @@ async function startWithoutDaemon() {
|
|
|
86745
86826
|
if (!newConfig.platforms || newConfig.platforms.length === 0) {
|
|
86746
86827
|
throw new Error("No platforms configured. Run with --setup to configure.");
|
|
86747
86828
|
}
|
|
86748
|
-
let
|
|
86829
|
+
let startupWarnings = [];
|
|
86749
86830
|
try {
|
|
86750
|
-
|
|
86831
|
+
startupWarnings = resolvePlatformMcpPosture(newConfig.platforms, newConfig.mcpServers).warnings;
|
|
86751
86832
|
} catch (err) {
|
|
86752
86833
|
console.error(red(` ❌ ${err instanceof Error ? err.message : String(err)}`));
|
|
86753
86834
|
process.exit(1);
|
|
86754
86835
|
}
|
|
86755
|
-
|
|
86836
|
+
startupWarnings.push(...presetOverrideWarnings);
|
|
86837
|
+
for (const w of startupWarnings)
|
|
86756
86838
|
console.warn(w);
|
|
86757
86839
|
const config = newConfig;
|
|
86758
86840
|
const firstPlatformConfig = config.platforms[0];
|
|
@@ -86914,7 +86996,7 @@ async function startWithoutDaemon() {
|
|
|
86914
86996
|
}
|
|
86915
86997
|
}
|
|
86916
86998
|
});
|
|
86917
|
-
for (const w of
|
|
86999
|
+
for (const w of startupWarnings) {
|
|
86918
87000
|
ui.addLog({ level: "warn", component: "config", message: w });
|
|
86919
87001
|
}
|
|
86920
87002
|
setLogHandler((level, component, message, sessionId) => {
|
|
@@ -86963,9 +87045,7 @@ async function startWithoutDaemon() {
|
|
|
86963
87045
|
configureAuditLog(platformConfig.id, resolveAuditLogEnabled(platformConfig.auditLog, `platforms[${platformConfig.id}].auditLog`));
|
|
86964
87046
|
session.addPlatform(platformConfig.id, client, {
|
|
86965
87047
|
overhead: {
|
|
86966
|
-
|
|
86967
|
-
stickyMessage: resolveOverheadVisibility(platformConfig.stickyMessage, `platforms[${platformConfig.id}].stickyMessage`),
|
|
86968
|
-
lifecycle: resolveOverheadVisibility(platformConfig.lifecycle, `platforms[${platformConfig.id}].lifecycle`),
|
|
87048
|
+
...resolvePresentationOverhead(platformConfig, `platforms[${platformConfig.id}]`),
|
|
86969
87049
|
turnMarker: resolveTurnMarker(platformConfig.turnMarker, platformConfig.turnMarkerEmoji, platformConfig.type, `platforms[${platformConfig.id}]`)
|
|
86970
87050
|
},
|
|
86971
87051
|
memory: resolveMemoryConfig(platformConfig.memory, `platforms[${platformConfig.id}].memory`),
|
|
@@ -86992,9 +87072,8 @@ async function startWithoutDaemon() {
|
|
|
86992
87072
|
configureAuditLog(dmConfig.id, resolveAuditLogEnabled(dmConfig.auditLog, `dm[${dmConfig.id}].auditLog`));
|
|
86993
87073
|
session.addPlatform(dmConfig.id, dmClient, {
|
|
86994
87074
|
overhead: {
|
|
86995
|
-
|
|
87075
|
+
...resolvePresentationOverhead(dmConfig, `dm[${dmConfig.id}]`),
|
|
86996
87076
|
stickyMessage: "hidden",
|
|
86997
|
-
lifecycle: resolveOverheadVisibility(dmConfig.lifecycle, `dm[${dmConfig.id}].lifecycle`),
|
|
86998
87077
|
turnMarker: resolveTurnMarker(dmConfig.turnMarker, dmConfig.turnMarkerEmoji, dmConfig.type, `dm[${dmConfig.id}]`)
|
|
86999
87078
|
},
|
|
87000
87079
|
memory: resolveMemoryConfig(dmConfig.memory, `dm[${dmConfig.id}].memory`),
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -169,6 +169,7 @@ What happens: every `audio/*` attachment (or a file with an audio extension when
|
|
|
169
169
|
| `permissionMode` | No | How tool-use is gated: `default` / `auto` / `bypass` (default: `default`). See [Permission Modes](#permission-modes). |
|
|
170
170
|
| `skipPermissions` | No | **Deprecated.** Use `permissionMode`. `true` maps to `bypass`, `false` to `default`. `permissionMode` wins when both are set. |
|
|
171
171
|
| `outboundFiles` | No | `send_file` settings: `{ enabled, maxBytes }` (defaults: enabled `true`, `maxBytes` 100 MB) |
|
|
172
|
+
| `mode` | No | Presentation preset: the baseline for `sessionHeader`, `stickyMessage` and `lifecycle`. `full` (default) is the behaviour described below; `assistant` hides all three, so Claude's reply is the whole thread. Any of those three fields set explicitly wins over the preset, so `mode: assistant` with `sessionHeader: full` is valid. `turnMarker` is machine-facing and deliberately not part of the preset. |
|
|
172
173
|
| `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
|
|
173
174
|
| `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
|
|
174
175
|
| `lifecycle` | No | Session status posts — idle warning, timeout, pause, resume, and the shutdown notice a deploy leaves in each open thread: `full` (default, all of them) / `minimal` (drops the idle warning, which predicts a timeout the next message would undo anyway) / `hidden` (none). Editing a status post the thread already has is not suppressed at any level: it adds no post and no notification, and leaving a stale "session idle" up across a restart would be worse. ⚠️ An **abnormal exit** (`[Exited: <code>]`, non-zero only) posts at every level including `hidden`: it is a failure report, and silencing it would make a session that died look exactly like one that finished. Note that `hidden` also removes the post a 🔄 reaction resumes from — sending a message in the thread still resumes it, and the channel sticky says so. |
|
|
@@ -194,6 +195,7 @@ What happens: every `audio/*` attachment (or a file with an audio extension when
|
|
|
194
195
|
| `permissionMode` | No | How tool-use is gated: `default` / `auto` / `bypass` (default: `default`). See [Permission Modes](#permission-modes). |
|
|
195
196
|
| `skipPermissions` | No | **Deprecated.** Use `permissionMode`. `true` maps to `bypass`, `false` to `default`. `permissionMode` wins when both are set. |
|
|
196
197
|
| `outboundFiles` | No | `send_file` settings: `{ enabled, maxBytes }` (defaults: enabled `true`, `maxBytes` 100 MB) |
|
|
198
|
+
| `mode` | No | Presentation preset: the baseline for `sessionHeader`, `stickyMessage` and `lifecycle`. `full` (default) is the behaviour described below; `assistant` hides all three, so Claude's reply is the whole thread. Any of those three fields set explicitly wins over the preset, so `mode: assistant` with `sessionHeader: full` is valid. `turnMarker` is machine-facing and deliberately not part of the preset. |
|
|
197
199
|
| `sessionHeader` | No | Per-thread header visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no header post) |
|
|
198
200
|
| `stickyMessage` | No | Channel sticky visibility: `full` (default) / `minimal` (status bar only) / `hidden` (no sticky, no bumping) |
|
|
199
201
|
| `lifecycle` | No | Session status posts — idle warning, timeout, pause, resume, and the shutdown notice a deploy leaves in each open thread: `full` (default, all of them) / `minimal` (drops the idle warning, which predicts a timeout the next message would undo anyway) / `hidden` (none). Editing a status post the thread already has is not suppressed at any level: it adds no post and no notification, and leaving a stale "session idle" up across a restart would be worse. ⚠️ An **abnormal exit** (`[Exited: <code>]`, non-zero only) posts at every level including `hidden`: it is a failure report, and silencing it would make a session that died look exactly like one that finished. Note that `hidden` also removes the post a 🔄 reaction resumes from — sending a message in the thread still resumes it, and the channel sticky says so. |
|
|
@@ -203,6 +205,56 @@ What happens: every `audio/*` attachment (or a file with an audio extension when
|
|
|
203
205
|
| `reconnectPolicy` | No | What happens when reconnection attempts run out: `retry` (default — log, cool down 60s, reset the counter and keep trying; recovers with no supervisor) or `exit` (leave through the graceful shutdown path and exit non-zero, for `Restart=always` deployments). The bot never stays alive with a dead socket either way. |
|
|
204
206
|
| `auditLog` | No | Append-only audit trail of what the bot executed for this platform — tool calls (incl. subagents), session lifecycle, security-relevant commands, plan approvals. One JSONL stream per platform under `~/.claude-threads/audit/` (override: `CLAUDE_THREADS_AUDIT_DIR`), files `0600`. The bot never deletes it — rotation/retention is the operator's job (logrotate, SIEM ingestion). See [Audit log](#audit-log). Default off. |
|
|
205
207
|
|
|
208
|
+
### Presentation presets (`mode`)
|
|
209
|
+
|
|
210
|
+
A platform entry carries three fields that decide how much of the bot's own
|
|
211
|
+
scaffolding a channel sees: `sessionHeader`, `stickyMessage` and `lifecycle`.
|
|
212
|
+
They are useful separately, but almost nobody wants to tune them one by one.
|
|
213
|
+
Two deployments asked for the same combination in nearly the same words: the
|
|
214
|
+
reply, and nothing else.
|
|
215
|
+
|
|
216
|
+
`mode` names that combination.
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
platforms:
|
|
220
|
+
- id: assistant-channel
|
|
221
|
+
type: slack
|
|
222
|
+
mode: assistant # no header, no sticky, no lifecycle notices
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Preset | `sessionHeader` | `stickyMessage` | `lifecycle` |
|
|
226
|
+
|--------|-----------------|-----------------|-------------|
|
|
227
|
+
| `full` (default) | `full` | `full` | `full` |
|
|
228
|
+
| `assistant` | `hidden` | `hidden` | `hidden` |
|
|
229
|
+
|
|
230
|
+
A field set explicitly always wins over the preset, so you can start from a
|
|
231
|
+
preset and keep one thing:
|
|
232
|
+
|
|
233
|
+
```yaml
|
|
234
|
+
mode: assistant
|
|
235
|
+
sessionHeader: full # replies only, but keep the per-thread header
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
One caveat worth knowing if you ran the setup wizard. It writes `sessionHeader`
|
|
239
|
+
and `stickyMessage` into your config whenever your answer differed from the
|
|
240
|
+
default, and those are explicit fields, so they win. Adding `mode: assistant`
|
|
241
|
+
to such a config moves `lifecycle` and leaves the other two where the wizard
|
|
242
|
+
put them. Startup warns and names the fields when that happens; remove them to
|
|
243
|
+
let the preset apply.
|
|
244
|
+
|
|
245
|
+
Two more things worth knowing:
|
|
246
|
+
|
|
247
|
+
- The preset is expanded when the config loads, so everything downstream reads
|
|
248
|
+
the same three concrete values it always did. Omitting `mode` resolves to
|
|
249
|
+
exactly the old defaults, which means an existing config behaves identically.
|
|
250
|
+
- `turnMarker` is **not** part of any preset. It is a signal for integrations
|
|
251
|
+
reading the channel rather than something a human sees, so it is not
|
|
252
|
+
something you would switch off because you want a quieter channel.
|
|
253
|
+
|
|
254
|
+
An abnormal exit is still reported under `mode: assistant`, for the reason
|
|
255
|
+
given in the `lifecycle` row above: a session that died must not look like one
|
|
256
|
+
that finished.
|
|
257
|
+
|
|
206
258
|
### Direct Channel Mode
|
|
207
259
|
|
|
208
260
|
`directChannelMode: true` turns the configured channel into a single, always-on conversation with the bot:
|
package/package.json
CHANGED