negotium 0.6.13 → 0.6.17

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 (31) hide show
  1. package/dist/agent-helpers.js +760 -142
  2. package/dist/agent-helpers.js.map +13 -12
  3. package/dist/{chunk-sr48tc2c.js → chunk-ax6tqx0s.js} +2 -2
  4. package/dist/{chunk-sr48tc2c.js.map → chunk-ax6tqx0s.js.map} +2 -2
  5. package/dist/hosted-agent.js +64 -3
  6. package/dist/hosted-agent.js.map +4 -4
  7. package/dist/main.js +783 -297
  8. package/dist/main.js.map +18 -17
  9. package/dist/mcp-factories.js +850 -261
  10. package/dist/mcp-factories.js.map +13 -12
  11. package/dist/registry.js +1 -1
  12. package/dist/runtime/cron/background-sessions.ts +5 -2
  13. package/dist/runtime/src/agents/archiver.ts +8 -5
  14. package/dist/runtime/src/agents/claude-provider.ts +88 -1
  15. package/dist/runtime/src/agents/idle-compact.ts +178 -0
  16. package/dist/runtime/src/node-host.ts +8 -1
  17. package/dist/runtime/src/runtime/background-sessions.ts +21 -8
  18. package/dist/runtime/src/runtime/turn-event-stream.ts +54 -1
  19. package/dist/runtime/src/runtime/turn-runner.ts +63 -0
  20. package/dist/runtime/src/topics/lifecycle.ts +2 -0
  21. package/dist/runtime/src/topics/session.ts +60 -3
  22. package/dist/runtime/src/version.ts +1 -1
  23. package/dist/types/packages/core/src/agents/archiver.d.ts +2 -2
  24. package/dist/types/packages/core/src/agents/idle-compact.d.ts +28 -0
  25. package/dist/types/packages/core/src/runtime/background-sessions.d.ts +10 -2
  26. package/dist/types/packages/core/src/runtime/turn-event-stream.d.ts +14 -0
  27. package/dist/types/packages/core/src/runtime/turn-runner.d.ts +2 -0
  28. package/dist/types/packages/core/src/topics/session.d.ts +11 -0
  29. package/dist/types/packages/core/src/version.d.ts +1 -1
  30. package/dist/types/packages/module-cron/src/background-sessions.d.ts +1 -1
  31. package/package.json +1 -1
@@ -8,6 +8,7 @@
8
8
  import { rmSync } from "node:fs";
9
9
  import { runArchiverTurn } from "#agents/archiver";
10
10
  import { cancelIdleArchiveForTopic } from "#agents/idle-archiver";
11
+ import { cancelIdleCompactForTopic } from "#agents/idle-compact";
11
12
  import { cancelSubagentWatchForDeletedTopic } from "#agents/mcp-tools/spawn-subagent";
12
13
  import {
13
14
  MIN_MEMORY_ARCHIVE_EXCHANGES,
@@ -59,6 +60,7 @@ async function abortAndWaitForTopic(topicId: string): Promise<boolean> {
59
60
  // Drop first: the dying turn's finally block must not dispatch queued work.
60
61
  interSessionQueue.drop(topicId);
61
62
  cancelIdleArchiveForTopic(topicId);
63
+ cancelIdleCompactForTopic(topicId);
62
64
  const aborted = abortRoom(topicId);
63
65
  if (!aborted) return true;
64
66
 
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import { ACTIVE_TASK_TEMPLATE, estimateConversationTokens } from "#agents/compaction-support";
8
8
  import { archiveActiveTopicForMemory, cancelIdleArchiveForTopic } from "#agents/idle-archiver";
9
+ import { cancelIdleCompactForTopic } from "#agents/idle-compact";
9
10
  import { runAgent } from "#agents/index";
10
11
  import { MIN_MEMORY_ARCHIVE_EXCHANGES } from "#agents/memory-archive-policy";
11
12
  import { resolveCompactionExecution, resolveModelForAgent } from "#agents/model-catalog";
@@ -40,7 +41,10 @@ import {
40
41
  beginRuntimeTopicMaintenance,
41
42
  type RuntimeTopicMaintenanceHandle,
42
43
  } from "#storage/runtime-topic-state";
43
- import { cancelRuntimeUserTurnRequestsBeforeEpoch } from "#storage/runtime-turn-requests";
44
+ import {
45
+ cancelRuntimeUserTurnRequestsBeforeEpoch,
46
+ getRuntimeUserTurnRequest,
47
+ } from "#storage/runtime-turn-requests";
44
48
  import { archiveConversationEvents } from "#storage/topic-archive";
45
49
  import { isLegacySharedGeneral } from "#topics/personal-general";
46
50
  import type { AgentKind, EffortLevel } from "#types";
@@ -62,6 +66,8 @@ export const AUTO_FORK_COMPACTION_TOKENS = 28_000;
62
66
  export interface RestartTopicSessionResult {
63
67
  text: string;
64
68
  isError?: boolean;
69
+ /** True when a non-preemptive compact bailed because a turn was already in flight (see `preemptive`). */
70
+ busy?: boolean;
65
71
  }
66
72
 
67
73
  export interface CompactSummaryRequest {
@@ -79,6 +85,15 @@ export interface CompactSummaryRequest {
79
85
  export interface CompactTopicSessionOptions {
80
86
  summarize?: (request: CompactSummaryRequest) => Promise<string>;
81
87
  cleanupOldRollouts?: typeof cleanupTopicRolloutsFromEntries;
88
+ /**
89
+ * When `false`, never interrupt work already in flight: instead of calling
90
+ * `fenceTopicWork` (which aborts local/remote turns and cancels queued
91
+ * requests), bail out as soon as maintenance reveals any turn is running or
92
+ * queued. Defaults to `true`, preserving `/compact`'s existing preemptive
93
+ * behavior — a human invoking it has implicitly accepted the interruption.
94
+ * Automatic/idle-triggered compaction must pass `false`.
95
+ */
96
+ preemptive?: boolean;
82
97
  }
83
98
 
84
99
  export interface CompactedRolloutRequest extends Omit<CompactSummaryRequest, "source"> {
@@ -135,6 +150,27 @@ async function fenceTopicWork(
135
150
  return maintenance.isOwned() ? null : "Topic maintenance ownership was lost. Try again.";
136
151
  }
137
152
 
153
+ /** True when any turn is running (this process or a remote node) or queued for this topic. */
154
+ function hasTopicWorkInFlight(topicId: string): boolean {
155
+ return Boolean(
156
+ getRoomQuery(topicId) || getRuntimeTurnLease(topicId) || getRuntimeUserTurnRequest(topicId),
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Non-preemptive counterpart to `fenceTopicWork`: never abort or cancel
162
+ * anything. `beginRuntimeTopicMaintenance` unconditionally bumps the topic's
163
+ * epoch just by being called, so callers must check *before* acquiring it
164
+ * (skip the bump entirely in the common busy case) and check again
165
+ * immediately *after* acquiring it (the authoritative read — once maintenance
166
+ * is held, `claimRuntimeTurnLease`'s own `WHERE NOT EXISTS (...maintenance)`
167
+ * guarantees no *new* turn can start, so anything found here was already
168
+ * running before we got here). Returns `true` when it is safe to proceed.
169
+ */
170
+ function topicIsQuiescedForNonPreemptiveWork(topicId: string): boolean {
171
+ return !hasTopicWorkInFlight(topicId);
172
+ }
173
+
138
174
  /**
139
175
  * Reset provider-native and provider-neutral context without deleting the
140
176
  * topic or its visible message history. Mirrors Otium's `/new` contract.
@@ -164,6 +200,7 @@ export async function restartTopicSession(
164
200
  const fenceError = await fenceTopicWork(topicId, maintenance);
165
201
  if (fenceError) return { text: fenceError, isError: true };
166
202
  cancelIdleArchiveForTopic(topicId);
203
+ cancelIdleCompactForTopic(topicId);
167
204
  const rawArchivePaths: string[] = [];
168
205
  try {
169
206
  for (const participantUserId of new Set([
@@ -731,12 +768,32 @@ export async function compactTopicSession(
731
768
  );
732
769
  if (!owner) return { text: "Only the topic owner can compact the session.", isError: true };
733
770
 
771
+ const preemptive = options.preemptive ?? true;
772
+ if (!preemptive && hasTopicWorkInFlight(topicId)) {
773
+ // Fast path: skip acquiring maintenance at all (which would otherwise
774
+ // unconditionally bump the topic epoch) in the common busy case.
775
+ return { text: "A turn is active or queued; compaction skipped.", isError: true, busy: true };
776
+ }
777
+
734
778
  const maintenance = beginRuntimeTopicMaintenance(topicId);
735
779
  if (!maintenance) return { text: "Topic maintenance is already in progress.", isError: true };
736
780
 
737
781
  try {
738
- const fenceError = await fenceTopicWork(topicId, maintenance);
739
- if (fenceError) return { text: fenceError, isError: true };
782
+ if (preemptive) {
783
+ const fenceError = await fenceTopicWork(topicId, maintenance);
784
+ if (fenceError) return { text: fenceError, isError: true };
785
+ } else if (!topicIsQuiescedForNonPreemptiveWork(topicId)) {
786
+ // Authoritative check, now that no *new* turn can start under this
787
+ // epoch. Never abort or cancel anything already running — bail instead.
788
+ return {
789
+ text: "A turn is active or queued; compaction skipped.",
790
+ isError: true,
791
+ busy: true,
792
+ };
793
+ } else if (!maintenance.isOwned()) {
794
+ return { text: "Topic maintenance ownership was lost. Try again.", isError: true };
795
+ }
796
+ cancelIdleCompactForTopic(topicId);
740
797
 
741
798
  const agent = (topic.agent ?? "maestro") as AgentKind;
742
799
  const registry = getRegistry(agent);
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.6.13";
1
+ export const NEGOTIUM_VERSION = "0.6.17";
@@ -73,7 +73,7 @@ export interface ArchiverHost {
73
73
  }
74
74
  export interface ArchiverRuntime {
75
75
  runArchiverTurn(params: RunArchiverTurnParams): boolean;
76
- listActiveMemoryArchiverSessions(userId: string): BackgroundSessionDto[];
76
+ listActiveMemoryArchiverSessions(userId: string, allUsers?: boolean): BackgroundSessionDto[];
77
77
  }
78
78
  export declare function createArchiverRuntime(host: ArchiverHost): ArchiverRuntime;
79
79
  /**
@@ -82,4 +82,4 @@ export declare function createArchiverRuntime(host: ArchiverHost): ArchiverRunti
82
82
  */
83
83
  export declare function findSummaryFile(storage: ArchiverStorageHost, topicTitle: string, date: string, sinceMs: number, topicId?: string): string | null;
84
84
  export declare function runArchiverTurn(params: RunArchiverTurnParams): boolean;
85
- export declare function listActiveMemoryArchiverSessions(userId: string): BackgroundSessionDto[];
85
+ export declare function listActiveMemoryArchiverSessions(userId: string, allUsers?: boolean): BackgroundSessionDto[];
@@ -0,0 +1,28 @@
1
+ import { getTopicStats } from "../storage/token-stats";
2
+ import type { RestartTopicSessionResult } from "../topics/session";
3
+ type IdleCompactStatus = "scheduled" | "disabled" | "busy" | "topic-not-found" | "not-ai-invited" | "mention-only-channel" | "no-owner" | "below-threshold" | "compacted" | "failed";
4
+ export interface IdleCompactOptions {
5
+ isBusy?: (topicId: string) => boolean;
6
+ onBusy?: (topicId: string, userId: string) => void;
7
+ minContextPercent?: number;
8
+ /** Injected for tests; defaults to the real `getTopicStats`. */
9
+ getStats?: typeof getTopicStats;
10
+ /** Injected for tests; defaults to the real `compactTopicSession`. */
11
+ compact?: (topicId: string, userId: string, reason: string) => Promise<RestartTopicSessionResult>;
12
+ }
13
+ /** Cancel a pending idle-compact timer, e.g. before a topic is reset or deleted. */
14
+ export declare function cancelIdleCompactForTopic(topicId: string): boolean;
15
+ export declare function idleCompactDelayMs(): number;
16
+ /** Percentage (0–100) of the provider's context window an idle topic must reach before it is auto-compacted. */
17
+ export declare function idleCompactMinContextPercent(): number;
18
+ export declare function idleCompactEnabled(): boolean;
19
+ /**
20
+ * Schedule a check, `idleCompactDelayMs()` after the topic's last turn, that
21
+ * compacts the session if (and only if) it is still idle and its context has
22
+ * grown past the percent-of-window threshold. Runs alongside the memory
23
+ * idle-archiver — this only trims what the model sees next turn; it does not
24
+ * touch memory.
25
+ */
26
+ export declare function scheduleIdleCompactForTopic(topicId: string, userId: string): IdleCompactStatus;
27
+ export declare function runIdleCompactForTopic(topicId: string, userId: string, options?: IdleCompactOptions): Promise<IdleCompactStatus>;
28
+ export {};
@@ -1,5 +1,5 @@
1
1
  import type { BackgroundSessionDto } from "../types/api";
2
- export type BackgroundSessionProvider = (userId: string) => BackgroundSessionDto[];
2
+ export type BackgroundSessionProvider = (userId: string, allUsers?: boolean) => BackgroundSessionDto[];
3
3
  export interface TransientBackgroundSessionHandle {
4
4
  id: string;
5
5
  update(status: string, step?: string): void;
@@ -16,4 +16,12 @@ export declare function backgroundSessionProgress(topicId: string, queryId: stri
16
16
  status: string;
17
17
  steps: string[];
18
18
  };
19
- export declare function listBackgroundSessionsForUser(userId: string): BackgroundSessionDto[];
19
+ /**
20
+ * `allUsers` widens every section — memory archiver, compact/transient, and
21
+ * cron — past the caller's own participation. Meant for single-owner
22
+ * surfaces (terminal, Telegram) where "my view" and "everything on this
23
+ * node" are the same person anyway, so idle background upkeep (and compact,
24
+ * manual or idle-triggered) triggered under a different local userId is
25
+ * still visible.
26
+ */
27
+ export declare function listBackgroundSessionsForUser(userId: string, allUsers?: boolean): BackgroundSessionDto[];
@@ -16,6 +16,20 @@ export type StreamAgentOutcome = {
16
16
  } | {
17
17
  kind: "provider-error";
18
18
  error: string;
19
+ }
20
+ /**
21
+ * The provider's terminal result carried no assistant text at all — the
22
+ * "silent turn" bug: a session-level race (e.g. a queued background-task
23
+ * notification flushing into the CLI session at the same instant a new
24
+ * user turn is dispatched) makes the underlying CLI self-interrupt within
25
+ * ~1-2s and report a trivial zero-usage "success". Distinct from
26
+ * provider-error so the caller can retry once before surfacing a failure —
27
+ * a manual resend has reliably produced a normal reply in every observed
28
+ * case.
29
+ */
30
+ | {
31
+ kind: "empty-response";
32
+ error: string;
19
33
  };
20
34
  export interface TurnEventStreamHooks {
21
35
  appendSystemMessage: (topicId: string, text: string) => MessageDto;
@@ -197,6 +197,8 @@ export interface StartAiTurnParams extends AiTurnExecutionOptions {
197
197
  /** Durable rows whose envelopes are already present in _userMessages. */
198
198
  _durableRequestIds?: string[];
199
199
  _sessionRetried?: boolean;
200
+ /** Internal only. Caps the empty-provider-response retry to a single attempt. */
201
+ _emptyResponseRetried?: boolean;
200
202
  }
201
203
  export interface TriggerTopicAiTurnOptions extends AiTurnExecutionOptions {
202
204
  /** Run the injected turn without writing the inject prompt as a visible message. */
@@ -8,6 +8,8 @@ export declare const AUTO_FORK_COMPACTION_TOKENS = 28000;
8
8
  export interface RestartTopicSessionResult {
9
9
  text: string;
10
10
  isError?: boolean;
11
+ /** True when a non-preemptive compact bailed because a turn was already in flight (see `preemptive`). */
12
+ busy?: boolean;
11
13
  }
12
14
  export interface CompactSummaryRequest {
13
15
  topicId: string;
@@ -23,6 +25,15 @@ export interface CompactSummaryRequest {
23
25
  export interface CompactTopicSessionOptions {
24
26
  summarize?: (request: CompactSummaryRequest) => Promise<string>;
25
27
  cleanupOldRollouts?: typeof cleanupTopicRolloutsFromEntries;
28
+ /**
29
+ * When `false`, never interrupt work already in flight: instead of calling
30
+ * `fenceTopicWork` (which aborts local/remote turns and cancels queued
31
+ * requests), bail out as soon as maintenance reveals any turn is running or
32
+ * queued. Defaults to `true`, preserving `/compact`'s existing preemptive
33
+ * behavior — a human invoking it has implicitly accepted the interruption.
34
+ * Automatic/idle-triggered compaction must pass `false`.
35
+ */
36
+ preemptive?: boolean;
26
37
  }
27
38
  export interface CompactedRolloutRequest extends Omit<CompactSummaryRequest, "source"> {
28
39
  entries: ConversationEntry[];
@@ -1 +1 @@
1
- export declare const NEGOTIUM_VERSION = "0.6.13";
1
+ export declare const NEGOTIUM_VERSION = "0.6.17";
@@ -1,2 +1,2 @@
1
1
  import { type BackgroundSessionDto } from "@negotium/core/cron-host";
2
- export declare function listCronBackgroundSessions(userId: string): BackgroundSessionDto[];
2
+ export declare function listCronBackgroundSessions(userId: string, allUsers?: boolean): BackgroundSessionDto[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "negotium",
3
- "version": "0.6.13",
3
+ "version": "0.6.17",
4
4
  "type": "module",
5
5
  "description": "Install the Negotium multi-agent runtime and CLI with one package",
6
6
  "license": "Apache-2.0",