@volter/supercode-teams 0.2.169 → 0.2.170
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.
|
@@ -14,6 +14,13 @@ export function syncStatusLines(sync, { indent = ' ' } = {}) {
|
|
|
14
14
|
for (const item of problems.slice(0, SHOWN)) lines.push(`${indent}${item.harness} ${item.session_id}${item.rule_id ? ` · ${item.rule_id}` : ''} · ${when(item.at)} · ${reason(item)}`);
|
|
15
15
|
if (problems.length > SHOWN) lines.push(`${indent}… and ${problems.length - SHOWN} more (--json lists them all)`);
|
|
16
16
|
} else lines.push('Not syncing none');
|
|
17
|
+
const unbound = sync.not_bound ?? [];
|
|
18
|
+
// A live session that is not bound cannot be typed into from the team; each says why.
|
|
19
|
+
if (unbound.length) {
|
|
20
|
+
const count = sync.not_bound_count ?? unbound.length;
|
|
21
|
+
lines.push(`Not bound (${count} live session${count === 1 ? '' : 's'}${count > unbound.length ? `, ${unbound.length} shown` : ''}):`);
|
|
22
|
+
for (const item of unbound.slice(0, SHOWN)) lines.push(`${indent}${item.harness} ${item.session_id} · ${item.reason}`);
|
|
23
|
+
}
|
|
17
24
|
if (sync.queued_captures) lines.push(`Queued ${sync.queued_captures} capture${sync.queued_captures === 1 ? '' : 's'} waiting for the server`);
|
|
18
25
|
if (sync.last_error) lines.push(`Last error ${when(sync.last_error.at)} · ${reason(sync.last_error)}`);
|
|
19
26
|
return lines;
|
package/bin/workspace.mjs
CHANGED
|
@@ -249,7 +249,7 @@ async function authCommand({ flags, sub = 'status', rest, home, contexts, output
|
|
|
249
249
|
say(account.machine ? `Machine ${account.machine.name} · ${account.machine.connection ?? 'unknown'} · sharing ${account.machine.folder ?? '?'}${account.machine.service ? ` (${account.machine.service})` : ' (runs while `teams connect` does)'}` : 'Machine not connected (supercode teams connect)');
|
|
250
250
|
// Only trouble (a session not syncing, an error in the last day) is worth lines here; `teams status` shows the rest.
|
|
251
251
|
const sync = account.machine?.sync;
|
|
252
|
-
if (sync?.not_syncing.length || sync?.last_error?.at > Date.now() - 86_400_000) for (const line of syncStatusLines(sync)) say(line);
|
|
252
|
+
if (sync?.not_syncing.length || sync?.not_bound?.length || sync?.last_error?.at > Date.now() - 86_400_000) for (const line of syncStatusLines(sync)) say(line);
|
|
253
253
|
return;
|
|
254
254
|
}
|
|
255
255
|
if (sub === 'switch') {
|
package/connector/index.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface ConnectorConfigurationValue {
|
|
|
66
66
|
export interface SyncErrorSummary { code: string | null; message: string; at: number }
|
|
67
67
|
export interface SyncProblem extends SyncErrorSummary { harness: string; session_id: string; rule_id: string | null }
|
|
68
68
|
/** What `supercode teams status` shows: sessions not syncing (most recent first) and the last error; null without state. */
|
|
69
|
-
export declare function readSyncStatus(stateDir: string): { not_syncing: SyncProblem[]; last_error: SyncErrorSummary | null; queued_captures: number } | null;
|
|
69
|
+
export declare function readSyncStatus(stateDir: string): { not_syncing: SyncProblem[]; not_bound: { harness: string; session_id: string; reason: string }[]; not_bound_count: number; last_error: SyncErrorSummary | null; queued_captures: number } | null;
|
|
70
70
|
|
|
71
71
|
export declare class TeamsConnector {
|
|
72
72
|
constructor(options: TeamsConnectorOptions);
|
package/connector/index.mjs
CHANGED
|
@@ -24,7 +24,7 @@ const errorSummary=(error,at)=>error==null?null:{code:typeof error.code==='strin
|
|
|
24
24
|
export function readSyncStatus(stateDir){
|
|
25
25
|
let state;try{state=JSON.parse(readFileSync(join(stateDir,'connector.json'),'utf8'));}catch{return null;}
|
|
26
26
|
const status=state?.sync_status;
|
|
27
|
-
return {not_syncing:Array.isArray(status?.problems)?status.problems:[],last_error:status?.last_error??null,queued_captures:Array.isArray(state?.outbox)?state.outbox.length:0};
|
|
27
|
+
return {not_syncing:Array.isArray(status?.problems)?status.problems:[],not_bound:Array.isArray(status?.unbound)?status.unbound:[],not_bound_count:Number.isSafeInteger(status?.unbound_count)?status.unbound_count:(Array.isArray(status?.unbound)?status.unbound.length:0),last_error:status?.last_error??null,queued_captures:Array.isArray(state?.outbox)?state.outbox.length:0};
|
|
28
28
|
}
|
|
29
29
|
// A tool call still running when the core reads a session has no result yet; the core answers it with a synthesized
|
|
30
30
|
// placeholder (crates/interchange session/helpers.rs ensure_tool_results_paired: a `tool` message for that call id,
|
|
@@ -139,6 +139,8 @@ export class TeamsConnector {
|
|
|
139
139
|
this.lastErrorValue=error;const known=sameError(status.last_error,summary);if(!known)status.last_error=summary;
|
|
140
140
|
if(!listed||!known)this.saveQuietly();
|
|
141
141
|
}
|
|
142
|
+
/** The live sessions that could not be bound, and why, kept for `teams status`; written only when the list changes. */
|
|
143
|
+
noteUnbound(){const status=this.syncStatus(),next=this.nativeEffects.notBound(SYNC_PROBLEM_LIMIT),count=this.nativeEffects.unbound.size;if(JSON.stringify(status.unbound??[])===JSON.stringify(next)&&status.unbound_count===count)return;status.unbound=next;status.unbound_count=count;this.saveQuietly();}
|
|
142
144
|
clearSessionProblem(harness,nativeSessionId){
|
|
143
145
|
const status=this.state.sync_status;if(!status?.problems?.some(item=>item.harness===harness&&item.session_id===nativeSessionId))return;
|
|
144
146
|
status.problems=status.problems.filter(item=>!(item.harness===harness&&item.session_id===nativeSessionId));this.saveQuietly();
|
|
@@ -179,6 +181,9 @@ export class TeamsConnector {
|
|
|
179
181
|
this.lastHeartbeatAt = this.clock.nowMs();
|
|
180
182
|
// The sessions someone is following now (a viewer or channel open on them) skip the capture floors.
|
|
181
183
|
if (Array.isArray(reply?.watched)) this.watched = new Set(reply.watched.map(item => `${item.harness}:${item.native_id}`));
|
|
184
|
+
// A binding the server could not store (no session with its native id) is said, not lost.
|
|
185
|
+
this.nativeEffects.refused(reply?.unbound_bindings);
|
|
186
|
+
this.noteUnbound();
|
|
182
187
|
this.save();
|
|
183
188
|
});
|
|
184
189
|
beating = next.catch(() => {});
|
|
@@ -8,19 +8,30 @@ const within = (root, path) => { const name = relative(root, path); return name
|
|
|
8
8
|
const LIMIT = 1024 * 1024;
|
|
9
9
|
|
|
10
10
|
export class NativeEffects {
|
|
11
|
-
constructor(connector) { this.c = connector; this.bindings = new Map(); this.attachments = new Map(); this.ownedRuntimes = new Set(); }
|
|
11
|
+
constructor(connector) { this.c = connector; this.bindings = new Map(); this.attachments = new Map(); this.ownedRuntimes = new Set(); this.unbound = new Map(); }
|
|
12
12
|
async discover(descriptor, ruleId) {
|
|
13
13
|
const c = this.c, locator = descriptor.locator;
|
|
14
|
-
if (!descriptor.live_endpoint?.startsWith('supercode-live://')
|
|
14
|
+
if (!descriptor.live_endpoint?.startsWith('supercode-live://')) return;
|
|
15
|
+
// A session with a live endpoint that ends up unbound says why (`teams status`, Not bound): it cannot be typed into.
|
|
16
|
+
const key = `${locator.harness}:${locator.session_id}`, unbound = reason => { this.unbound.set(key, { harness: locator.harness, session_id: locator.session_id, reason, seen: true }); };
|
|
17
|
+
if (!descriptor.cwd) return unbound('the session names no working folder');
|
|
18
|
+
if (!c.core.attachManagedRuntime || !c.core.runtimeCapabilities) return unbound('this harness client cannot attach to live runtimes');
|
|
15
19
|
const id = `bind_${digest(`${c.machineId}:${c.state.incarnation_id}:${locator.harness}:${locator.session_id}`).slice(0,32)}`;
|
|
16
20
|
const params = { harness: locator.harness, runtime_id: locator.session_id, cwd: descriptor.cwd, base_url: descriptor.live_endpoint };
|
|
17
21
|
const existing = this.bindings.get(id);
|
|
18
|
-
if (existing && JSON.stringify(existing.params) === JSON.stringify(params)) { existing.seen = true; existing.ruleId = ruleId; return; }
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
if (existing && JSON.stringify(existing.params) === JSON.stringify(params)) { existing.seen = true; existing.ruleId = ruleId; this.unbound.delete(key); return; }
|
|
23
|
+
let capabilities;
|
|
24
|
+
try { ({ capabilities } = await c.core.runtimeCapabilities(params)); } catch (error) { return unbound(`reading its runtime's capabilities failed: ${String(error?.message ?? error).slice(0, 200)}`); }
|
|
25
|
+
const missing = ['attach_existing_process', 'stream_events'].filter(name => !capabilities[name]);
|
|
26
|
+
if (missing.length) return unbound(`its runtime does not offer ${missing.join(' or ')}`);
|
|
27
|
+
// A live endpoint whose runtime is gone (its receipt's process ended) has nothing to bind: that is quiet, and the
|
|
28
|
+
// next discovery no longer offers it. Any other failure to attach is said.
|
|
22
29
|
let runtime;
|
|
23
|
-
try { runtime = await c.core.attachManagedRuntime(params); } catch {
|
|
30
|
+
try { runtime = await c.core.attachManagedRuntime(params); } catch (error) {
|
|
31
|
+
const message = String(error?.message ?? error);
|
|
32
|
+
if (error?.code === 'not_found' || error?.sdkCode === 'not_found') { this.unbound.delete(key); return; }
|
|
33
|
+
return unbound(`attaching to its runtime failed: ${message.slice(0, 200)}`);
|
|
34
|
+
}
|
|
24
35
|
try {
|
|
25
36
|
const actions = ['attach_observe'];
|
|
26
37
|
if (capabilities.send_input) actions.push('interact');
|
|
@@ -31,10 +42,21 @@ export class NativeEffects {
|
|
|
31
42
|
native_runtime_id: runtime.handle.runtime_id, native_session_id: locator.session_id,
|
|
32
43
|
harness: locator.harness, state: 'running', observed_at: c.clock.nowMs(), native_capabilities: actions,
|
|
33
44
|
} });
|
|
45
|
+
this.unbound.delete(key);
|
|
34
46
|
} finally { await runtime.close(); } // This backend's close detaches the observer only.
|
|
35
47
|
}
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
/** The server said it holds no session for these bindings: they stay unbound, and say so. */
|
|
49
|
+
refused(items) {
|
|
50
|
+
for (const item of items ?? []) {
|
|
51
|
+
const binding = this.bindings.get(item.id);
|
|
52
|
+
if (!binding) continue;
|
|
53
|
+
this.unbound.set(`${binding.descriptor.harness}:${binding.descriptor.native_session_id}`, { harness: binding.descriptor.harness, session_id: binding.descriptor.native_session_id, reason: 'the server holds no session with this native id from this machine', seen: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Every live session that is not bound, and why. */
|
|
57
|
+
notBound(limit = 50) { return [...this.unbound.values()].slice(0, limit).map(({ seen, ...item }) => item); }
|
|
58
|
+
beginDiscovery() { for (const binding of this.bindings.values()) binding.seen = false; for (const item of this.unbound.values()) item.seen = false; }
|
|
59
|
+
endDiscovery() { for (const [id, binding] of this.bindings) if (!binding.seen) this.bindings.delete(id); for (const [key, item] of this.unbound) if (!item.seen) this.unbound.delete(key); }
|
|
38
60
|
published() { return [...this.bindings.values()].map(binding => ({ ...binding.descriptor, observed_at: this.c.clock.nowMs() })); }
|
|
39
61
|
async prepare(effect) {
|
|
40
62
|
if (effect.kind === 'continue') return this.prepareContinuation(effect);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.170",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|