scalix 1.5.0 → 1.5.2

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.
@@ -28,7 +28,15 @@ export function parseRetryAfter(value) {
28
28
  return Math.max(0, date - Date.now());
29
29
  return undefined;
30
30
  }
31
- const DEFAULT_TIMEOUT_MS = 120_000;
31
+ // Per-request abort. 300s matches the platform's own proxy walls; the old
32
+ // 120s default aborted the client before a slow first token could arrive on
33
+ // CPU-served models (the request was fine, we hung up on it). For reference,
34
+ // the OpenAI and Anthropic SDKs both default to 600s.
35
+ const DEFAULT_TIMEOUT_MS = (() => {
36
+ const v = process.env.SCALIX_REQUEST_TIMEOUT_MS;
37
+ const n = v ? parseInt(v, 10) : NaN;
38
+ return Number.isFinite(n) ? n : 0;
39
+ })();
32
40
  // The capability probe only decides whether a turn streams. It must never be
33
41
  // the reason a turn is slow to start, so it gives up quickly and falls back to
34
42
  // the conservative answer.
@@ -116,8 +124,8 @@ export class ScalixClient {
116
124
  for (;;) {
117
125
  const prepared = this.prepareRequest(req);
118
126
  const body = JSON.stringify(prepared);
119
- const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
120
- const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
127
+ const timeoutSignal = this.timeoutMs > 0 ? AbortSignal.timeout(this.timeoutMs) : undefined;
128
+ const combined = signal && timeoutSignal ? AbortSignal.any([signal, timeoutSignal]) : (signal || timeoutSignal);
121
129
  const response = await fetch(url, {
122
130
  method: 'POST',
123
131
  headers: {
@@ -127,7 +135,7 @@ export class ScalixClient {
127
135
  'x-client-type': 'cli',
128
136
  },
129
137
  body,
130
- signal: combined,
138
+ signal: combined || undefined,
131
139
  });
132
140
  if (!response.ok) {
133
141
  const text = await response.text().catch(() => '');
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Crew board context (scalix-org roadmap/SCALIX-CREW.md, T2.2/T2.3).
3
+ *
4
+ * BOARD.md is a READ-ONLY projection of a crew's notice board, written into
5
+ * the workspace by whoever runs the session (the orchestrator's CrewHydrator
6
+ * for cloud sessions; the cockpit for local ones) and refreshed before each
7
+ * turn. The engine never writes it — posting goes through the
8
+ * scalix_crew_board_post platform tool, the board's only write path.
9
+ *
10
+ * Durable lessons do not live here either: a member that learns something
11
+ * permanent from a board note promotes it into project memory (/remember),
12
+ * deliberately. Board = news; memory = knowledge.
13
+ */
14
+ import fs from 'node:fs/promises';
15
+ import path from 'node:path';
16
+ export const BOARD_FILE = 'BOARD.md';
17
+ /** A board is news, not a document dump — cap what lands in context so a
18
+ * runaway board cannot crowd the task out of the window. */
19
+ export const BOARD_CONTEXT_MAX_CHARS = 16 * 1024;
20
+ /** Read the workspace board projection. Null when absent or empty — those
21
+ * mean "this session has no crew board", which is different from a board
22
+ * with no notes (that renders its own explicit empty state). */
23
+ export async function readBoardFile(cwd) {
24
+ try {
25
+ const raw = await fs.readFile(path.join(cwd, BOARD_FILE), 'utf-8');
26
+ const trimmed = raw.trim();
27
+ if (!trimmed)
28
+ return null;
29
+ return trimmed.length > BOARD_CONTEXT_MAX_CHARS
30
+ ? trimmed.slice(0, BOARD_CONTEXT_MAX_CHARS) + '\n…(board truncated for context)'
31
+ : trimmed;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ export function boardToContext(board) {
38
+ if (!board)
39
+ return '';
40
+ return [
41
+ '## Crew Board (read-only projection)',
42
+ 'Shared coordination state from your crew, current as of session start.',
43
+ 'Post updates with the scalix_crew_board_post tool. Never edit BOARD.md —',
44
+ 'it is overwritten on every refresh and your edit would be silently lost.',
45
+ '',
46
+ board,
47
+ ].join('\n');
48
+ }
49
+ /**
50
+ * Turn-start freshness for a session whose system prompt is assembled once:
51
+ * tracks the last projection seen and, when the file moves, hands back a
52
+ * system note carrying the new copy for the conversation history. One file
53
+ * read per turn; a quiet board costs nothing else.
54
+ */
55
+ export class BoardTurnRefresher {
56
+ cwd;
57
+ lastSeen = null;
58
+ constructor(cwd) {
59
+ this.cwd = cwd;
60
+ }
61
+ /** Baseline — call once near session start, after the system prompt has
62
+ * embedded the board, so the first turnNote() only fires on real change. */
63
+ async prime() {
64
+ this.lastSeen = await readBoardFile(this.cwd);
65
+ }
66
+ /** Null when nothing changed (or the file vanished — a removed projection
67
+ * is a session-plumbing event, not news to tell the model about). */
68
+ async turnNote() {
69
+ const current = await readBoardFile(this.cwd);
70
+ if (current === this.lastSeen)
71
+ return null;
72
+ this.lastSeen = current;
73
+ if (current === null)
74
+ return null;
75
+ return [
76
+ '## Crew Board (updated since your last turn — supersedes earlier copies)',
77
+ '',
78
+ current,
79
+ ].join('\n');
80
+ }
81
+ }
Binary file