@volter/teams 0.2.10 → 0.2.12

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/server/events.mjs CHANGED
@@ -15,3 +15,23 @@ export function sseRoom(res,bytes,{cap=SSE_BACKLOG_BYTES,patienceMs=SSE_PATIENCE
15
15
  res.on('drain',drained);res.on('close',ended);
16
16
  });
17
17
  }
18
+ // Open event streams are counted per team install: a credential on one machine (a machine credential's own machine, or
19
+ // the enrolled machine of this team its request names in X-Supercode-Machine), else the credential with no machine.
20
+ // An install's connector holds one published-events stream that its local readers share (dispatchers, agents, apps on
21
+ // that machine read through its door), and its people open a few viewers beside it (session follows, a catalogue
22
+ // watch): one stream, one more while a dropped one is still being noticed, about five viewers, and headroom makes 10.
23
+ // A credential's streams over all its machines stop at 40, so a leaked token cannot hold the server's connections.
24
+ export const STREAMS_PER_INSTALL=10, STREAMS_PER_CREDENTIAL=40;
25
+ // A slot frees only when another stream of the install closes; a refused reader asks again no sooner than this.
26
+ export const STREAM_RETRY_SECONDS=30;
27
+ /** Count one open stream for `auth`'s install, refusing past the limits; resolves the release (idempotent). */
28
+ export async function holdStream(api,auth,req){
29
+ const named=String(req.headers['x-supercode-machine']??'');
30
+ let machine=auth.kind==='machine'?auth.machine_id:'';
31
+ if(!machine&&/^mach_[A-Za-z0-9_-]{1,64}$/.test(named)&&await api.store.one('SELECT 1 FROM machines WHERE team_id=? AND id=? AND revoked_at IS NULL',[auth.team_id,named]))machine=named;
32
+ const keys=[`install:${auth.credential_id}\0${machine}`,`credential:${auth.credential_id}`],limits=[STREAMS_PER_INSTALL,STREAMS_PER_CREDENTIAL];
33
+ if(keys.some((key,at)=>(api.openStreams.get(key)??0)>=limits[at]))throw Object.assign(new Error(`too many open event streams for this ${machine?'machine':'credential'}`),{status:429,code:'rate_limited',details:{retry_after_seconds:STREAM_RETRY_SECONDS,streams_per_install:STREAMS_PER_INSTALL,machine:machine||null}});
34
+ for(const key of keys)api.openStreams.set(key,(api.openStreams.get(key)??0)+1);
35
+ let held=true;
36
+ return ()=>{if(!held)return;held=false;for(const key of keys){const next=(api.openStreams.get(key)??1)-1;if(next>0)api.openStreams.set(key,next);else api.openStreams.delete(key);}};
37
+ }
@@ -1,5 +1,5 @@
1
1
  // A retained machine transition log for Teams mailbox subscriptions.
2
- import { writeSse } from './events.mjs';
2
+ import { writeSse, holdStream } from './events.mjs';
3
3
 
4
4
  export function machineTransition(api, team, machine, linked) {
5
5
  // Serialize the read/change/event across asynchronous store calls. A replaced
@@ -37,11 +37,9 @@ export async function reconcileMachinePresence(api) {
37
37
  export async function machineEventStream({api,req,res,auth,url,requestId,fail}) {
38
38
  const after = url.searchParams.get('after') ?? req.headers['last-event-id'];
39
39
  if (after != null && (!/^\d+$/.test(after) || !Number.isSafeInteger(Number(after)))) fail(400,'invalid_request','after must be a machine event cursor');
40
- const count = api.openStreams.get(auth.credential_id) ?? 0;
41
- if (count >= 10) fail(429,'rate_limited','too many open streams');
42
- api.openStreams.set(auth.credential_id,count+1);
40
+ const release = await holdStream(api,auth,req);
43
41
  let closed=false, cursor=after==null?null:Number(after), work=Promise.resolve();
44
- const close=()=>{ if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);api.openStreams.set(auth.credential_id,Math.max(0,(api.openStreams.get(auth.credential_id)??1)-1));res.end(); };
42
+ const close=()=>{ if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);release();res.end(); };
45
43
  const write=event=>{if(closed)return;if(res.writableLength>1024*1024){close();return;}writeSse(res,event);};
46
44
  const drain=async()=>{
47
45
  if(closed)return;
@@ -3,7 +3,7 @@ import { appAuthentication } from './apps.mjs';
3
3
  import { isAdmin } from './auth.mjs';
4
4
  import { exact } from './request-json.mjs';
5
5
  import { canonicalJson } from './wire.mjs';
6
- import { sseRoom, writeSse } from './events.mjs';
6
+ import { sseRoom, writeSse, holdStream } from './events.mjs';
7
7
  import { APP_EVENTS, PUBLISHABLE_EVENTS, eventReadAction } from './event-catalogue.mjs';
8
8
  import { EVENT_DATA_LIMIT } from '../event-limits.mjs';
9
9
 
@@ -159,10 +159,9 @@ async function eventStream({api,req,res,auth,url,requestId}){
159
159
  if([...url.searchParams.keys()].some(key=>!['after','source','kinds'].includes(key))||query.kinds?.some(kind=>!APP_EVENTS.includes(kind)))fail(400,'invalid_request','invalid event subscription');
160
160
  auth=await liveAuth(api,auth);let codec=cursorCodec(api,auth,query);
161
161
  const after=url.searchParams.get('after')??req.headers['last-event-id'];let cursor=after==null?null:codec.decode(after);
162
- const count=api.openStreams.get(auth.credential_id)??0;if(count>=10)fail(429,'rate_limited','too many event subscriptions');
163
- api.openStreams.set(auth.credential_id,count+1);
162
+ const release=await holdStream(api,auth,req);
164
163
  let closed=false,running=null,dirty=false;
165
- const close=()=>{if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);api.openStreams.set(auth.credential_id,Math.max(0,(api.openStreams.get(auth.credential_id)??1)-1));res.end();};
164
+ const close=()=>{if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);release();res.end();};
166
165
  const terminal=(type,reason)=>{if(!closed)writeSse(res,{type,reason,terminal:true});close();};
167
166
  const write=async event=>{if(closed)return false;const room=await sseRoom(res,Buffer.byteLength(JSON.stringify(event)));if(closed||room==='closed')return false;if(room==='slow'){terminal('resync_required','slow_reader');return false;}writeSse(res,event);return true;};
168
167
  const readable=async event=>{
@@ -163,6 +163,16 @@ const automationImage = seed => `https://brand.volter.ai/bot/svg?seed=${encodeUR
163
163
  * may draw its own faces; `personImage`/`agentImage` choose the default pictures. One author's run within five minutes
164
164
  * is `continued`, and each entry has a short local `timeLabel`.
165
165
  */
166
+ /**
167
+ * A Room member's words, without the envelope Teams types ahead of them into the session ('[Room "<name>" ("room_…");
168
+ * sender "<name>" ("prn_…")] ' then the words). Only that exact quote is taken off, and only when the sender it names is
169
+ * the principal recorded as typing the turn; any other text, a forged or quoted envelope included, is shown as typed.
170
+ */
171
+ function roomWords(text, principal) {
172
+ const m = /^\s*\[Room "(?:[^"\\]|\\.)*" \("room_[A-Za-z0-9]+"\); sender "(?:[^"\\]|\\.)*" \("(prn_[A-Za-z0-9]+)"\)\]\s*/.exec(String(text ?? ''));
173
+ return m && principal && m[1] === principal ? String(text).slice(m[0].length) : text;
174
+ }
175
+
166
176
  export function withPeople(transcript, messages, { agentName = null, sessionId = '', personImage: person = personImage, agentImage: agent = agentImage, locale = [] } = {}) {
167
177
  const byTime = new Map();
168
178
  for (const message of messages) {
@@ -204,7 +214,8 @@ export function withPeople(transcript, messages, { agentName = null, sessionId =
204
214
  const seed = name ? (metadata.source_principal_id || name) : '';
205
215
  const via = typeof metadata.source_via === 'string' ? metadata.source_via.trim() : '';
206
216
  const source = ({ room: 'Room', app: 'App', terminal: 'Terminal', api: 'API' })[via] || via || 'Session';
207
- return { ...entry, actor: { ...(seed ? { id: seed } : {}), name: name ?? 'Anonymous', anonymous: !name, source, image: person(seed), kind: 'person' } };
217
+ const text = via === 'room' ? roomWords(entry.text, metadata.source_principal_id) : entry.text;
218
+ return { ...entry, text, actor: { ...(seed ? { id: seed } : {}), name: name ?? 'Anonymous', anonymous: !name, source, image: person(seed), kind: 'person' } };
208
219
  });
209
220
  let last = null;
210
221
  return people.map((entry) => {
@@ -669,6 +669,8 @@
669
669
  .scui-configuration-value-input { flex:1; min-width:calc(120px * var(--scui-scaling)); width:100%; }
670
670
  .scui-configuration-button { padding:calc(5px * var(--scui-scaling)) calc(12px * var(--scui-scaling)); }
671
671
  .scui-configuration-alert { color:var(--scui-danger,var(--scui-fg)); }
672
+ .scui-configuration-proposed { color:var(--scui-muted,var(--scui-fg)); }
673
+ .scui-configuration-proposed-link { color:var(--scui-accent,currentColor); }
672
674
  .scui-configuration-details { margin-top:calc(20px * var(--scui-scaling)); color:var(--scui-muted); }
673
675
  .scui-configuration-summary { cursor:pointer; }
674
676
  .scui-configuration-input { box-sizing:border-box; min-width:0; max-width:100%; padding:calc(7px * var(--scui-scaling)) calc(9px * var(--scui-scaling)); }
@@ -704,6 +706,43 @@
704
706
  .scui-settings-text { margin:0 }
705
707
  .scui-settings-footer { display:flex; gap:calc(6px * var(--scui-scaling)); justify-content:flex-end; margin-top:auto; padding:calc(8px * var(--scui-scaling)); border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }
706
708
  .scui-settings-action { padding:calc(6px * var(--scui-scaling)) calc(9px * var(--scui-scaling)); border-radius:var(--scui-button-radius, var(--scui-radius-md)) }
709
+ .scui-setup-jobs { display:grid; gap:6px; margin:0 0 8px; padding:0; list-style:none; }
710
+ .scui-timeline { display:grid; gap:8px; min-width:0; font-size:12.5px; }
711
+ .scui-timeline-header { display:flex; gap:4px; align-items:center; }
712
+ .scui-timeline-button { min-width:28px; padding:2px 8px; }
713
+ .scui-timeline-grid { position:relative; display:grid; gap:2px; min-width:0; }
714
+ .scui-timeline-axis { position:relative; height:18px; margin-left:var(--scui-timeline-label-width,180px); border-bottom:1px solid var(--scui-border); }
715
+ .scui-timeline-tick { position:absolute; transform:translateX(-50%); color:var(--scui-muted); font-size:11px; white-space:nowrap; }
716
+ .scui-timeline-lane { display:grid; grid-template-columns:var(--scui-timeline-label-width,180px) 1fr; align-items:center; min-height:24px; }
717
+ .scui-timeline-lane-label { overflow:hidden; padding-right:8px; color:var(--scui-fg); text-overflow:ellipsis; white-space:nowrap; }
718
+ .scui-timeline-track { position:relative; height:20px; border-radius:4px; background:var(--scui-bg-raised); }
719
+ .scui-timeline-block { position:absolute; top:2px; height:16px; min-width:2px; overflow:hidden; padding:0 4px; border:0; border-radius:3px; background:var(--scui-accent); color:var(--scui-bg); font-size:11px; text-align:left; white-space:nowrap; }
720
+ .scui-timeline-block[data-kind="forecast"] { border:1px dashed var(--scui-accent); background:transparent; color:var(--scui-fg); }
721
+ .scui-timeline-block[data-kind="due"] { width:3px !important; padding:0; background:var(--scui-danger); }
722
+ .scui-timeline-block[data-kind="failed"] { background:var(--scui-danger); }
723
+ .scui-timeline-block[data-kind="cancelled"] { background:var(--scui-muted); }
724
+ .scui-timeline-block[data-kind="waiting"] { background:var(--scui-warning); }
725
+ .scui-timeline-block[data-kind="idle"] { background:var(--scui-fill); color:var(--scui-fg); }
726
+ .scui-timeline-block-label { pointer-events:none; }
727
+ .scui-timeline-overlay { position:absolute; inset:18px 0 0 var(--scui-timeline-label-width,180px); width:calc(100% - var(--scui-timeline-label-width,180px)); height:calc(100% - 18px); overflow:visible; pointer-events:none; }
728
+ .scui-timeline-arrow { fill:none; stroke:var(--scui-muted); stroke-width:1.2; }
729
+ .scui-timeline-now { stroke:var(--scui-danger); stroke-width:1; }
730
+ .scui-timeline-series { display:grid; grid-template-columns:var(--scui-timeline-label-width,180px) 1fr; align-items:center; height:48px; }
731
+ .scui-timeline-series-chart { width:100%; height:44px; }
732
+ .scui-timeline-series-line { fill:none; stroke:var(--scui-accent); stroke-width:1.5; }
733
+ .scui-calendar { display:grid; gap:8px; font-size:12.5px; }
734
+ .scui-calendar-header { display:flex; gap:8px; align-items:center; }
735
+ .scui-calendar-title { min-width:140px; text-align:center; }
736
+ .scui-calendar-grid { display:grid; grid-template-columns:repeat(7,minmax(0,1fr)); gap:2px; margin:0; padding:0; list-style:none; }
737
+ .scui-calendar-day { display:grid; align-content:start; gap:2px; min-height:84px; padding:4px; border-radius:4px; background:var(--scui-bg-raised); }
738
+ .scui-calendar-day[data-outside="true"] { opacity:.45; }
739
+ .scui-calendar-day[data-today="true"] { outline:1px solid var(--scui-accent); }
740
+ .scui-calendar-date { color:var(--scui-muted); font-size:11px; }
741
+ .scui-calendar-entry { overflow:hidden; padding:1px 4px; border:0; border-radius:3px; background:var(--scui-accent); color:var(--scui-bg); font-size:11px; text-align:left; text-overflow:ellipsis; white-space:nowrap; }
742
+ .scui-calendar-entry[data-kind="deadline"] { background:var(--scui-danger); }
743
+ .scui-calendar-entry[data-kind="window"] { background:var(--scui-warning); }
744
+ .scui-calendar-entry[data-kind="job"] { background:var(--scui-fill); color:var(--scui-fg); }
745
+ .scui-calendar-empty { margin:0; color:var(--scui-muted); }
707
746
  }
708
747
  @layer scui.components {
709
748
  /* Team view: sessions, machines, pane access and a pane's terminal over a Teams server. */