@peakflo/agent-harness 0.2.14 → 0.2.15-rc.1

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.
@@ -67,7 +67,9 @@ relist) {
67
67
  Date.now() - checkedAt < SUCCESS_CACHE_TTL_MS)
68
68
  return null;
69
69
  const priorFailure = failed?.get(key);
70
- if (priorFailure && Date.now() < priorFailure.nextRetryAt) {
70
+ if (priorFailure &&
71
+ !relist?.has(name) &&
72
+ Date.now() < priorFailure.nextRetryAt) {
71
73
  return {
72
74
  name,
73
75
  reason: priorFailure.reason,
@@ -11,11 +11,11 @@ import { type McpServerToolListing, type McpToolsetMember } from './mcp-toolset'
11
11
  * request before the prompt.
12
12
  *
13
13
  * WHEN A LISTING IS REUSED. Only while all of these still hold:
14
- * - the lease carries `mcpToolScope` (the speaker and the Space whose live
15
- * permissions the gateway applies). An older control plane does not send
14
+ * - the lease carries `mcpToolScope` (the speaker, Space and effective grants
15
+ * whose live permissions the gateway applies). An older control plane does not send
16
16
  * it, and then nothing is reused, so a list never crosses speakers;
17
17
  * - the scope is the one the listing was taken under. A different speaker in
18
- * a shared chat drops every stored listing of the session;
18
+ * a shared chat or a changed grant set drops every stored listing;
19
19
  * - the server's own fingerprint (name, upstream URL, allowlist) is the same;
20
20
  * - the server is still configured at the URL the listing was taken from;
21
21
  * - the listing is younger than `MCP_TOOL_STORE_MAX_AGE_MS` and than the
@@ -36,10 +36,11 @@ import { type McpServerToolListing, type McpToolsetMember } from './mcp-toolset'
36
36
  * success cache, so a turn a few minutes after the last one skips the
37
37
  * preflight too (the baseline turn `91c442db` came 6 minutes after the one
38
38
  * before it and spent 9.7 s in it). Short, because a catalogue change that is
39
- * not in the lease (an app connected, or a grant revoked, for the same speaker)
40
- * shows only after this; Pi also replaces the cached tools as soon as it
41
- * connects to a server. A revoked tool can stay listed for up to this long,
42
- * but the gateway refuses the call: a stale name, never extra access.
39
+ * not represented in the lease (for example an upstream tool schema change)
40
+ * shows only after this. Integration grant changes invalidate the lease scope
41
+ * key on the next turn. Within a running turn a revoked tool can stay listed,
42
+ * but the gateway refuses its next call. Pi also replaces its cached metadata
43
+ * whenever it connects to the server.
43
44
  */
44
45
  export declare const MCP_TOOL_STORE_MAX_AGE_MS: number;
45
46
  interface StoredServer {
@@ -48,6 +49,7 @@ interface StoredServer {
48
49
  }
49
50
  interface StoredToolset {
50
51
  scopeKey: string;
52
+ members: Map<string, string>;
51
53
  servers: Map<string, StoredServer>;
52
54
  }
53
55
  /** Shared by the harness across the per-turn runners, keyed by session id. */
@@ -16,11 +16,11 @@ const mcp_toolset_1 = require("./mcp-toolset");
16
16
  * request before the prompt.
17
17
  *
18
18
  * WHEN A LISTING IS REUSED. Only while all of these still hold:
19
- * - the lease carries `mcpToolScope` (the speaker and the Space whose live
20
- * permissions the gateway applies). An older control plane does not send
19
+ * - the lease carries `mcpToolScope` (the speaker, Space and effective grants
20
+ * whose live permissions the gateway applies). An older control plane does not send
21
21
  * it, and then nothing is reused, so a list never crosses speakers;
22
22
  * - the scope is the one the listing was taken under. A different speaker in
23
- * a shared chat drops every stored listing of the session;
23
+ * a shared chat or a changed grant set drops every stored listing;
24
24
  * - the server's own fingerprint (name, upstream URL, allowlist) is the same;
25
25
  * - the server is still configured at the URL the listing was taken from;
26
26
  * - the listing is younger than `MCP_TOOL_STORE_MAX_AGE_MS` and than the
@@ -41,10 +41,11 @@ const mcp_toolset_1 = require("./mcp-toolset");
41
41
  * success cache, so a turn a few minutes after the last one skips the
42
42
  * preflight too (the baseline turn `91c442db` came 6 minutes after the one
43
43
  * before it and spent 9.7 s in it). Short, because a catalogue change that is
44
- * not in the lease (an app connected, or a grant revoked, for the same speaker)
45
- * shows only after this; Pi also replaces the cached tools as soon as it
46
- * connects to a server. A revoked tool can stay listed for up to this long,
47
- * but the gateway refuses the call: a stale name, never extra access.
44
+ * not represented in the lease (for example an upstream tool schema change)
45
+ * shows only after this. Integration grant changes invalidate the lease scope
46
+ * key on the next turn. Within a running turn a revoked tool can stay listed,
47
+ * but the gateway refuses its next call. Pi also replaces its cached metadata
48
+ * whenever it connects to the server.
48
49
  */
49
50
  exports.MCP_TOOL_STORE_MAX_AGE_MS = 15 * 60_000;
50
51
  const MAX_STORED_SESSIONS = 256;
@@ -67,13 +68,21 @@ function reusableUntil(listing) {
67
68
  function planMcpToolReuse(store, sessionId, members, scope, configured, now = Date.now()) {
68
69
  const plan = { reused: {}, relist: new Set(), dropped: {} };
69
70
  const stored = store.get(sessionId);
70
- if (!stored)
71
+ if (!stored) {
72
+ // A server can reappear inside the preflight success/failure TTL. With no
73
+ // listing for this scope, probe it now rather than inherit that old hint.
74
+ if (scope)
75
+ for (const member of members)
76
+ plan.relist.add(member.name);
71
77
  return plan;
78
+ }
72
79
  const dropAll = (reason) => {
73
80
  for (const name of stored.servers.keys()) {
74
81
  plan.relist.add(name);
75
82
  plan.dropped[name] = reason;
76
83
  }
84
+ for (const member of members)
85
+ plan.relist.add(member.name);
77
86
  store.delete(sessionId);
78
87
  };
79
88
  if (!scope) {
@@ -81,7 +90,7 @@ function planMcpToolReuse(store, sessionId, members, scope, configured, now = Da
81
90
  return plan;
82
91
  }
83
92
  if (stored.scopeKey !== scopeKeyOf(scope)) {
84
- dropAll('speaker or space changed');
93
+ dropAll('tool scope changed');
85
94
  return plan;
86
95
  }
87
96
  const current = new Map(members.map((member) => [member.name, member]));
@@ -111,10 +120,13 @@ function planMcpToolReuse(store, sessionId, members, scope, configured, now = Da
111
120
  }
112
121
  plan.reused[name] = entry.listing;
113
122
  }
123
+ for (const member of members) {
124
+ if (stored.members.get(member.name) !== memberKey(member))
125
+ plan.relist.add(member.name);
126
+ }
114
127
  // A reuse counts as use: move the session to the newest end.
115
128
  store.delete(sessionId);
116
- if (stored.servers.size > 0)
117
- store.set(sessionId, stored);
129
+ store.set(sessionId, stored);
118
130
  return plan;
119
131
  }
120
132
  /**
@@ -129,8 +141,9 @@ function rememberMcpToolListings(store, sessionId, members, scope, listings) {
129
141
  const scopeKey = scopeKeyOf(scope);
130
142
  let stored = store.get(sessionId);
131
143
  if (!stored || stored.scopeKey !== scopeKey) {
132
- stored = { scopeKey, servers: new Map() };
144
+ stored = { scopeKey, members: new Map(), servers: new Map() };
133
145
  }
146
+ stored.members = new Map(members.map((member) => [member.name, memberKey(member)]));
134
147
  const current = new Map(members.map((member) => [member.name, member]));
135
148
  const kept = [];
136
149
  for (const [name, listing] of listings) {
@@ -142,8 +155,7 @@ function rememberMcpToolListings(store, sessionId, members, scope, listings) {
142
155
  }
143
156
  // Most recently used last, so the oldest session is evicted first.
144
157
  store.delete(sessionId);
145
- if (stored.servers.size > 0)
146
- store.set(sessionId, stored);
158
+ store.set(sessionId, stored);
147
159
  while (store.size > MAX_STORED_SESSIONS) {
148
160
  store.delete(store.keys().next().value);
149
161
  }
package/dist/types.d.ts CHANGED
@@ -546,8 +546,8 @@ export interface LeasedSession {
546
546
  turnId: string;
547
547
  runtime: RunnerRuntime;
548
548
  /**
549
- * Opaque. Changes when the speaker or the Space whose live permissions the
550
- * MCP gateway applies changes, so the tool catalogue may differ. The harness
549
+ * Opaque. Changes with the speaker, Space or effective integration grants
550
+ * (including tool limits), so the tool catalogue may differ. The harness
551
551
  * reuses a session's stored MCP tool listings on a later turn only while
552
552
  * this is the same. Optional and additive: absent (an older control plane)
553
553
  * means nothing is reused across turns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peakflo/agent-harness",
3
- "version": "0.2.14",
3
+ "version": "0.2.15-rc.1",
4
4
  "description": "Headless Peakflo coding-agent harness. One codebase, three hosting modes: a Daytona cloud sandbox, a customer VM, and the 20x desktop.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",