@dotdrelle/wiki-manager 0.15.48 → 0.15.50
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/README.md +34 -3
- package/docker-compose.yml +7 -1
- package/package.json +2 -2
- package/src/agent/graph.js +4 -36
- package/src/agent/graph.test.js +12 -54
- package/src/cli/wiki-manager.js +28 -4
- package/src/commands/slash.js +1 -1
- package/src/core/agentEvents.js +12 -1
- package/src/core/agentEvents.test.js +21 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +18 -0
- package/src/core/mcp.js +1 -1
- package/src/orchestrator/.fuse_hidden0000001c00000001 +316 -0
- package/src/orchestrator/agentRegistry.js +54 -0
- package/src/orchestrator/agentRegistry.test.js +75 -0
- package/src/runtime/approvals.js +13 -1
- package/src/runtime/approvals.test.js +44 -0
- package/src/runtime/client.js +2 -0
- package/src/runtime/delegation.js +1 -1
- package/src/runtime/runner.js +3 -3
- package/src/runtime/server.js +12 -4
- package/src/runtime/server.test.js +63 -1
- package/src/runtime/supervisor.js +18 -1
- package/src/runtime/supervisor.test.js +43 -0
- package/src/shell/LeftPane.tsx +17 -5
- package/src/shell/StartupScreen.tsx +9 -4
- package/src/shell/repl.js +1 -0
|
@@ -728,6 +728,57 @@ test('runtime server kill can target a specific run id', async (t) => {
|
|
|
728
728
|
}
|
|
729
729
|
});
|
|
730
730
|
|
|
731
|
+
test('runtime server kill purge without workspace is refused', async (t) => {
|
|
732
|
+
let clearedArgs = null;
|
|
733
|
+
let handle;
|
|
734
|
+
try {
|
|
735
|
+
handle = await startRuntimeServer({
|
|
736
|
+
host: '127.0.0.1',
|
|
737
|
+
port: 0,
|
|
738
|
+
store: {
|
|
739
|
+
dbPath: ':memory:',
|
|
740
|
+
getState: () => ({ status: 'idle' }),
|
|
741
|
+
listEvents: () => [],
|
|
742
|
+
interruptRuns: () => 0,
|
|
743
|
+
cancelActiveTasksForInterruptedRuns: () => 0,
|
|
744
|
+
clearWorkspaceState: (args) => {
|
|
745
|
+
clearedArgs = args;
|
|
746
|
+
return { runs: 0, events: 0, queue: 0 };
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
session: {},
|
|
750
|
+
run: async () => {},
|
|
751
|
+
});
|
|
752
|
+
} catch (err) {
|
|
753
|
+
if (err?.code === 'EPERM') {
|
|
754
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
throw err;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
const unscoped = await fetch(`http://127.0.0.1:${handle.port}/kill`, {
|
|
762
|
+
method: 'POST',
|
|
763
|
+
headers: { 'Content-Type': 'application/json' },
|
|
764
|
+
body: JSON.stringify({ purge: true }),
|
|
765
|
+
});
|
|
766
|
+
assert.equal(unscoped.status, 400);
|
|
767
|
+
assert.deepEqual(await unscoped.json(), { killed: false, reason: 'workspace_required' });
|
|
768
|
+
assert.equal(clearedArgs, null);
|
|
769
|
+
|
|
770
|
+
const scoped = await fetch(`http://127.0.0.1:${handle.port}/kill?workspace=docs`, {
|
|
771
|
+
method: 'POST',
|
|
772
|
+
headers: { 'Content-Type': 'application/json' },
|
|
773
|
+
body: JSON.stringify({ purge: true }),
|
|
774
|
+
});
|
|
775
|
+
assert.equal(scoped.status, 202);
|
|
776
|
+
assert.equal(clearedArgs.workspace, 'docs');
|
|
777
|
+
} finally {
|
|
778
|
+
await handle.close();
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
731
782
|
test('runtime server state exposes active run identity while running', async (t) => {
|
|
732
783
|
let releaseRun;
|
|
733
784
|
const context = {
|
|
@@ -1031,6 +1082,7 @@ test('runtime server exposes approval endpoint', async (t) => {
|
|
|
1031
1082
|
groupId: null,
|
|
1032
1083
|
planRevision: null,
|
|
1033
1084
|
approvalClasses: [],
|
|
1085
|
+
caller: null,
|
|
1034
1086
|
});
|
|
1035
1087
|
assert.deepEqual(await response.json(), { approved: true, runId: 'run-1', itemId: 'item-1' });
|
|
1036
1088
|
} finally {
|
|
@@ -1256,11 +1308,21 @@ test('runtime server control message handles approve and cancel intents during a
|
|
|
1256
1308
|
const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
1257
1309
|
method: 'POST',
|
|
1258
1310
|
headers: { 'Content-Type': 'application/json' },
|
|
1259
|
-
body: JSON.stringify({ action: 'message', input: 'valide tout' }),
|
|
1311
|
+
body: JSON.stringify({ action: 'message', input: 'valide tout', intent: 'approve' }),
|
|
1260
1312
|
});
|
|
1261
1313
|
assert.equal(approveResponse.status, 200);
|
|
1262
1314
|
assert.equal((await approveResponse.json()).kind, 'approve');
|
|
1263
1315
|
|
|
1316
|
+
// Free-text approval phrasing is NOT honored anymore: approval is an
|
|
1317
|
+
// explicit /approve (or the approval button), never a keyword match.
|
|
1318
|
+
const freeTextResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
1319
|
+
method: 'POST',
|
|
1320
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1321
|
+
body: JSON.stringify({ action: 'message', input: 'valide tout' }),
|
|
1322
|
+
});
|
|
1323
|
+
assert.equal(freeTextResponse.status, 200);
|
|
1324
|
+
assert.notEqual((await freeTextResponse.json()).kind, 'approve');
|
|
1325
|
+
|
|
1264
1326
|
const cancelResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, {
|
|
1265
1327
|
method: 'POST',
|
|
1266
1328
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -13,6 +13,7 @@ export function startActivitySupervisor(session, {
|
|
|
13
13
|
agentRegistryIntervalMs = registryIntervalFromEnv(),
|
|
14
14
|
agentRegistry = null,
|
|
15
15
|
callTool = callMcpTool,
|
|
16
|
+
refreshMcp = null,
|
|
16
17
|
} = {}) {
|
|
17
18
|
const pollBusy = new Set();
|
|
18
19
|
let stopped = false;
|
|
@@ -32,8 +33,24 @@ export function startActivitySupervisor(session, {
|
|
|
32
33
|
}
|
|
33
34
|
}, queueIntervalMs);
|
|
34
35
|
|
|
36
|
+
/*
|
|
37
|
+
The re-scan must probe the endpoints again, not trust a stale status.
|
|
38
|
+
|
|
39
|
+
The first discovery ran while the containers were still coming up, so the
|
|
40
|
+
production endpoint was marked "not connected" and its agent fell back to a
|
|
41
|
+
legacy placeholder. Re-scanning against that cached status kept the fallback
|
|
42
|
+
forever. `refreshMcp` re-resolves the endpoint states (and their tools)
|
|
43
|
+
before each discovery, so a container that came up is actually discovered.
|
|
44
|
+
*/
|
|
35
45
|
const agentRegistryTimer = agentRegistryIntervalMs > 0
|
|
36
|
-
? setInterval(() => {
|
|
46
|
+
? setInterval(async () => {
|
|
47
|
+
if (refreshMcp) {
|
|
48
|
+
try {
|
|
49
|
+
await refreshMcp(session);
|
|
50
|
+
} catch {
|
|
51
|
+
/* the discovery below still runs on the cached status */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
37
54
|
void discoverAgentsOnce(session, { registry, signal: runSignal });
|
|
38
55
|
}, agentRegistryIntervalMs)
|
|
39
56
|
: null;
|
|
@@ -246,6 +246,49 @@ test('startActivitySupervisor periodically re-scans the agent registry', async (
|
|
|
246
246
|
}
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
+
test('startActivitySupervisor refreshes MCP before each periodic re-scan', async () => {
|
|
250
|
+
/*
|
|
251
|
+
Without the refresh, the re-scan re-reads a cached "not connected" endpoint
|
|
252
|
+
status and keeps a legacy placeholder forever, so an agent whose container
|
|
253
|
+
came up after boot is never discovered. The refresh probes the endpoints
|
|
254
|
+
again before the scan.
|
|
255
|
+
*/
|
|
256
|
+
const session = {
|
|
257
|
+
mcp: {},
|
|
258
|
+
activities: {},
|
|
259
|
+
headlessPlan: null,
|
|
260
|
+
jobQueue: [],
|
|
261
|
+
};
|
|
262
|
+
let refreshes = 0;
|
|
263
|
+
let discoveries = 0;
|
|
264
|
+
const registry = {
|
|
265
|
+
async discover() {
|
|
266
|
+
discoveries += 1;
|
|
267
|
+
return [];
|
|
268
|
+
},
|
|
269
|
+
snapshot() {
|
|
270
|
+
return [];
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const supervisor = startActivitySupervisor(session, {
|
|
275
|
+
intervalMs: 1000,
|
|
276
|
+
queueIntervalMs: 1000,
|
|
277
|
+
agentRegistryIntervalMs: 10,
|
|
278
|
+
agentRegistry: registry,
|
|
279
|
+
refreshMcp: async () => {
|
|
280
|
+
refreshes += 1;
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await waitFor(() => refreshes >= 2);
|
|
286
|
+
assert.ok(discoveries >= 2);
|
|
287
|
+
} finally {
|
|
288
|
+
supervisor.stop();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
249
292
|
test('discoverAgentsOnce uses the session registry and returns discovered agents', async () => {
|
|
250
293
|
const expected = [{ agentInstanceId: 'a' }];
|
|
251
294
|
const registry = {
|
package/src/shell/LeftPane.tsx
CHANGED
|
@@ -822,14 +822,17 @@ export function LeftPane(props: {
|
|
|
822
822
|
const showWelcome = () => props.showWelcome && props.messages.length === 0;
|
|
823
823
|
return (
|
|
824
824
|
<box width={props.width} height="100%" flexDirection="column" padding={1} overflow="hidden">
|
|
825
|
-
|
|
825
|
+
{/*
|
|
826
|
+
The header keeps what changes the MEANING of what is typed — the mode,
|
|
827
|
+
and the transient hint. Workspace, version and connection states moved
|
|
828
|
+
to the foot of the pane: they are an ambient state one glances at, not
|
|
829
|
+
something read before every message, and at the top they pushed the
|
|
830
|
+
conversation down by a line for standing information.
|
|
831
|
+
*/}
|
|
832
|
+
<box height={2} flexDirection="column">
|
|
826
833
|
<box height={1} flexDirection="row" backgroundColor={modeColor()} paddingX={1}>
|
|
827
834
|
<text fg="#0B1020">{modeLabel()}</text>
|
|
828
835
|
</box>
|
|
829
|
-
<box height={1} flexDirection="row">
|
|
830
|
-
<text fg="#D6DEE8">{props.title}</text>
|
|
831
|
-
<text fg="#7F8C8D"> {props.statusLine}</text>
|
|
832
|
-
</box>
|
|
833
836
|
<box height={1} flexDirection="row">
|
|
834
837
|
{props.hintLine ? <text fg="#FBBF24">[ {props.hintLine} ]</text> : null}
|
|
835
838
|
</box>
|
|
@@ -860,6 +863,15 @@ export function LeftPane(props: {
|
|
|
860
863
|
onSubmit={props.submit}
|
|
861
864
|
onHeightChange={props.onInputHeightChange}
|
|
862
865
|
/>
|
|
866
|
+
{/*
|
|
867
|
+
Status foot. One line, as in the header before it, so the row budget of
|
|
868
|
+
`conversationRows` (height - 5 - input) is unchanged: the line moved,
|
|
869
|
+
it was not added.
|
|
870
|
+
*/}
|
|
871
|
+
<box height={1} flexDirection="row">
|
|
872
|
+
<text fg="#D6DEE8">{props.title}</text>
|
|
873
|
+
<text fg="#7F8C8D"> {props.statusLine}</text>
|
|
874
|
+
</box>
|
|
863
875
|
</box>
|
|
864
876
|
);
|
|
865
877
|
}
|
|
@@ -249,7 +249,15 @@ export function StartupScreen(props: {
|
|
|
249
249
|
return `${workspace} / ${profile}`;
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
/*
|
|
253
|
+
Usable width inside the panel.
|
|
254
|
+
|
|
255
|
+
The panel has no border any more, only `padding={2}`: 4 columns, not 6. The
|
|
256
|
+
two extra columns were the border's, and keeping them would leave the text
|
|
257
|
+
measured for a frame that is no longer drawn — every `fit()` truncating two
|
|
258
|
+
characters early for nothing.
|
|
259
|
+
*/
|
|
260
|
+
const innerWidth = createMemo(() => Math.max(40, panelWidth() - 4));
|
|
253
261
|
const shortcutHint = createMemo(() => {
|
|
254
262
|
const count = currentLength();
|
|
255
263
|
const quick = count === 1 ? '1 quick select' : `1-${count} quick select`;
|
|
@@ -267,9 +275,6 @@ export function StartupScreen(props: {
|
|
|
267
275
|
top={top()}
|
|
268
276
|
width={panelWidth()}
|
|
269
277
|
height={panelHeight()}
|
|
270
|
-
border
|
|
271
|
-
borderStyle="rounded"
|
|
272
|
-
borderColor="#8BD5CA"
|
|
273
278
|
backgroundColor="#111318"
|
|
274
279
|
padding={2}
|
|
275
280
|
flexDirection="column"
|
package/src/shell/repl.js
CHANGED
|
@@ -2115,6 +2115,7 @@ async function runTuiShell({ agent, packageJson, session, runtime = null }) {
|
|
|
2115
2115
|
url: runtime.url,
|
|
2116
2116
|
workspace: session.workspace ?? null,
|
|
2117
2117
|
...(kind === 'item' ? { itemId: id } : kind === 'approval' ? { approvalId: id } : { runId: id }),
|
|
2118
|
+
caller: 'shell-/approve',
|
|
2118
2119
|
});
|
|
2119
2120
|
conversationMessages(session).push({ role: 'command', content: `Approval ${result.approved ? 'accepted' : 'not found'}: ${id}` });
|
|
2120
2121
|
syncRuntimeState();
|