agent-relay-server 0.16.0 → 0.17.0
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/docs/openapi.json +101 -1
- package/package.json +2 -2
- package/public/index.html +75 -3
- package/src/automations.ts +2 -1
- package/src/config-store.ts +59 -0
- package/src/db.ts +22 -3
- package/src/managed-policy.ts +2 -1
- package/src/routes.ts +44 -0
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.16.0",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
|
@@ -5957,6 +5957,100 @@
|
|
|
5957
5957
|
]
|
|
5958
5958
|
}
|
|
5959
5959
|
},
|
|
5960
|
+
"/api/workspace-config": {
|
|
5961
|
+
"get": {
|
|
5962
|
+
"operationId": "getWorkspaceConfigRoute",
|
|
5963
|
+
"summary": "Get global workspace (isolated worktree) config",
|
|
5964
|
+
"tags": [
|
|
5965
|
+
"Other"
|
|
5966
|
+
],
|
|
5967
|
+
"description": "Returns the global workspace provisioning config `{ symlinkPaths: string[] }` — untracked files/filename patterns symlinked from the main checkout into each isolated worktree (the dashboard \"Workspace\" spawn option). Defaults seed `AGENTS.md` and `.claude-rig`; merged over defaults until set.",
|
|
5968
|
+
"responses": {
|
|
5969
|
+
"200": {
|
|
5970
|
+
"description": "Success",
|
|
5971
|
+
"content": {
|
|
5972
|
+
"application/json": {}
|
|
5973
|
+
}
|
|
5974
|
+
},
|
|
5975
|
+
"400": {
|
|
5976
|
+
"description": "Bad request",
|
|
5977
|
+
"content": {
|
|
5978
|
+
"application/json": {
|
|
5979
|
+
"schema": {
|
|
5980
|
+
"$ref": "#/components/schemas/Error"
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
},
|
|
5986
|
+
"security": [
|
|
5987
|
+
{
|
|
5988
|
+
"bearerAuth": []
|
|
5989
|
+
},
|
|
5990
|
+
{
|
|
5991
|
+
"tokenHeader": []
|
|
5992
|
+
},
|
|
5993
|
+
{
|
|
5994
|
+
"tokenQuery": []
|
|
5995
|
+
}
|
|
5996
|
+
]
|
|
5997
|
+
},
|
|
5998
|
+
"put": {
|
|
5999
|
+
"operationId": "putWorkspaceConfigRoute",
|
|
6000
|
+
"summary": "Set global workspace (isolated worktree) config",
|
|
6001
|
+
"tags": [
|
|
6002
|
+
"Other"
|
|
6003
|
+
],
|
|
6004
|
+
"description": "Updates the global workspace config. Body: `{ value: { symlinkPaths: [...] }, updatedBy? }`. Each entry is a relative path or glob pattern; plain names match files and directories, entries with `*?[]{}` are expanded against main. A path is only linked if it exists in main — missing entries are ignored at spawn time. Rejects absolute paths and `..` traversal (400). Versioned with full config history.",
|
|
6005
|
+
"requestBody": {
|
|
6006
|
+
"required": true,
|
|
6007
|
+
"content": {
|
|
6008
|
+
"application/json": {
|
|
6009
|
+
"schema": {
|
|
6010
|
+
"type": "object",
|
|
6011
|
+
"properties": {
|
|
6012
|
+
"value": {
|
|
6013
|
+
"type": "string"
|
|
6014
|
+
},
|
|
6015
|
+
"updatedBy": {
|
|
6016
|
+
"type": "string"
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
},
|
|
6023
|
+
"responses": {
|
|
6024
|
+
"200": {
|
|
6025
|
+
"description": "Success",
|
|
6026
|
+
"content": {
|
|
6027
|
+
"application/json": {}
|
|
6028
|
+
}
|
|
6029
|
+
},
|
|
6030
|
+
"400": {
|
|
6031
|
+
"description": "Bad request",
|
|
6032
|
+
"content": {
|
|
6033
|
+
"application/json": {
|
|
6034
|
+
"schema": {
|
|
6035
|
+
"$ref": "#/components/schemas/Error"
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
},
|
|
6041
|
+
"security": [
|
|
6042
|
+
{
|
|
6043
|
+
"bearerAuth": []
|
|
6044
|
+
},
|
|
6045
|
+
{
|
|
6046
|
+
"tokenHeader": []
|
|
6047
|
+
},
|
|
6048
|
+
{
|
|
6049
|
+
"tokenQuery": []
|
|
6050
|
+
}
|
|
6051
|
+
]
|
|
6052
|
+
}
|
|
6053
|
+
},
|
|
5960
6054
|
"/api/insights/config": {
|
|
5961
6055
|
"get": {
|
|
5962
6056
|
"operationId": "getInsightsConfigRoute",
|
|
@@ -6143,6 +6237,9 @@
|
|
|
6143
6237
|
},
|
|
6144
6238
|
"source": {
|
|
6145
6239
|
"type": "string"
|
|
6240
|
+
},
|
|
6241
|
+
"occurredAt": {
|
|
6242
|
+
"type": "string"
|
|
6146
6243
|
}
|
|
6147
6244
|
}
|
|
6148
6245
|
}
|
|
@@ -7217,6 +7314,9 @@
|
|
|
7217
7314
|
"maxAgeSeconds": {
|
|
7218
7315
|
"type": "string"
|
|
7219
7316
|
},
|
|
7317
|
+
"occurredAt": {
|
|
7318
|
+
"type": "string"
|
|
7319
|
+
},
|
|
7220
7320
|
"channel": {
|
|
7221
7321
|
"type": "string"
|
|
7222
7322
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"CONTRIBUTING.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"agent-relay-sdk": "0.2.
|
|
36
|
+
"agent-relay-sdk": "0.2.8"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"prepack": "bun run build:dashboard:bundle >&2",
|
package/public/index.html
CHANGED
|
@@ -125641,6 +125641,9 @@ function chatTimestamp(iso) {
|
|
|
125641
125641
|
minute: "2-digit"
|
|
125642
125642
|
});
|
|
125643
125643
|
}
|
|
125644
|
+
function messageEventTime(msg) {
|
|
125645
|
+
return new Date(msg.occurredAt ?? msg.createdAt).getTime();
|
|
125646
|
+
}
|
|
125644
125647
|
function dateSeparatorLabel(dateStr) {
|
|
125645
125648
|
const today = /* @__PURE__ */ new Date();
|
|
125646
125649
|
const [y, m, d] = dateStr.split("-").map(Number);
|
|
@@ -126444,7 +126447,7 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
|
|
|
126444
126447
|
const pointerStartRef = (0, import_react.useRef)(null);
|
|
126445
126448
|
const suppressBubbleClickRef = (0, import_react.useRef)(false);
|
|
126446
126449
|
const body = messageBody(msg);
|
|
126447
|
-
const time = chatTimestamp(msg.createdAt);
|
|
126450
|
+
const time = chatTimestamp(msg.occurredAt ?? msg.createdAt);
|
|
126448
126451
|
const attachments = messageAttachments(msg);
|
|
126449
126452
|
const reactions = groupedReactions(msg);
|
|
126450
126453
|
const receipt = isOutbound ? outboundReceipt(msg, peer) : null;
|
|
@@ -126580,7 +126583,7 @@ function sessionActivityStep(msg) {
|
|
|
126580
126583
|
kind: s.type,
|
|
126581
126584
|
label: typeof s.label === "string" ? s.label : void 0,
|
|
126582
126585
|
text: msg.body,
|
|
126583
|
-
ts:
|
|
126586
|
+
ts: messageEventTime(msg),
|
|
126584
126587
|
turnId: typeof s.turnId === "string" ? s.turnId : void 0
|
|
126585
126588
|
};
|
|
126586
126589
|
}
|
|
@@ -126674,7 +126677,7 @@ function buildTimeline(messages, statusEvents, createdAt, importedHistory = [])
|
|
|
126674
126677
|
continue;
|
|
126675
126678
|
}
|
|
126676
126679
|
raw.push({
|
|
126677
|
-
ts:
|
|
126680
|
+
ts: messageEventTime(msg),
|
|
126678
126681
|
entry: {
|
|
126679
126682
|
type: "message",
|
|
126680
126683
|
msg
|
|
@@ -153682,11 +153685,80 @@ function SettingsView() {
|
|
|
153682
153685
|
]
|
|
153683
153686
|
})]
|
|
153684
153687
|
}),
|
|
153688
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceSettings, {}),
|
|
153685
153689
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StewardSettings, {}),
|
|
153686
153690
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(VoiceSettings, {})
|
|
153687
153691
|
]
|
|
153688
153692
|
});
|
|
153689
153693
|
}
|
|
153694
|
+
var DEFAULT_WORKSPACE = { symlinkPaths: [] };
|
|
153695
|
+
function WorkspaceSettings() {
|
|
153696
|
+
const [config, setConfig] = (0, import_react.useState)(DEFAULT_WORKSPACE);
|
|
153697
|
+
const [status, setStatus] = (0, import_react.useState)("");
|
|
153698
|
+
(0, import_react.useEffect)(() => {
|
|
153699
|
+
api("GET", "/workspace-config").then((entry) => setConfig({
|
|
153700
|
+
...DEFAULT_WORKSPACE,
|
|
153701
|
+
...entry.value
|
|
153702
|
+
})).catch((e) => setStatus(e.message));
|
|
153703
|
+
}, []);
|
|
153704
|
+
async function save() {
|
|
153705
|
+
try {
|
|
153706
|
+
const saved = await api("PUT", "/workspace-config", config);
|
|
153707
|
+
setConfig({
|
|
153708
|
+
...DEFAULT_WORKSPACE,
|
|
153709
|
+
...saved.value
|
|
153710
|
+
});
|
|
153711
|
+
setStatus("Workspace config saved");
|
|
153712
|
+
} catch (e) {
|
|
153713
|
+
setStatus(e.message);
|
|
153714
|
+
}
|
|
153715
|
+
}
|
|
153716
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
|
|
153717
|
+
className: "space-y-3 rounded-lg border p-4",
|
|
153718
|
+
children: [
|
|
153719
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153720
|
+
className: "flex items-center gap-2",
|
|
153721
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FolderSymlink, { className: "w-4 h-4" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
153722
|
+
className: "text-sm font-semibold",
|
|
153723
|
+
children: "Isolated worktree symlinks"
|
|
153724
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
153725
|
+
className: "text-xs text-muted-foreground",
|
|
153726
|
+
children: [
|
|
153727
|
+
"Untracked files or filename patterns symlinked from main into each isolated worktree (the \"Workspace\" spawn option), one per line. Plain names like ",
|
|
153728
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "AGENTS.md" }),
|
|
153729
|
+
" or ",
|
|
153730
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: ".claude-rig" }),
|
|
153731
|
+
" match files and directories; entries with ",
|
|
153732
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("code", { children: ["*?[]", "{}"] }),
|
|
153733
|
+
" are expanded as globs. A path is only linked if it exists in main."
|
|
153734
|
+
]
|
|
153735
|
+
})] })]
|
|
153736
|
+
}),
|
|
153737
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
|
|
153738
|
+
label: "Paths / patterns",
|
|
153739
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
|
|
153740
|
+
rows: 5,
|
|
153741
|
+
placeholder: "AGENTS.md\n.claude-rig",
|
|
153742
|
+
value: config.symlinkPaths.join("\n"),
|
|
153743
|
+
onChange: (e) => setConfig({
|
|
153744
|
+
...config,
|
|
153745
|
+
symlinkPaths: lines(e.target.value)
|
|
153746
|
+
})
|
|
153747
|
+
})
|
|
153748
|
+
}),
|
|
153749
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153750
|
+
className: "flex items-center gap-2 pt-1",
|
|
153751
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
153752
|
+
onClick: save,
|
|
153753
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Save, { className: "w-4 h-4" }), "Save"]
|
|
153754
|
+
}), status && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
153755
|
+
className: "text-sm text-muted-foreground",
|
|
153756
|
+
children: status
|
|
153757
|
+
})]
|
|
153758
|
+
})
|
|
153759
|
+
]
|
|
153760
|
+
});
|
|
153761
|
+
}
|
|
153690
153762
|
function VoiceSettings() {
|
|
153691
153763
|
const voiceInputMode = useRelayStore((s) => s.voiceInputMode);
|
|
153692
153764
|
const setVoiceInputMode = useRelayStore((s) => s.setVoiceInputMode);
|
package/src/automations.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
ValidationError,
|
|
13
13
|
} from "./db";
|
|
14
14
|
import { createCommand } from "./commands-db";
|
|
15
|
-
import { getAgentProfile, getSpawnPolicy } from "./config-store";
|
|
15
|
+
import { getAgentProfile, getSpawnPolicy, workspaceSpawnParams } from "./config-store";
|
|
16
16
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
17
17
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
18
18
|
import type {
|
|
@@ -559,6 +559,7 @@ function dispatchOnDemandAutomation(
|
|
|
559
559
|
effort: selection.effort,
|
|
560
560
|
profile: policy.profile,
|
|
561
561
|
agentProfile,
|
|
562
|
+
...workspaceSpawnParams(),
|
|
562
563
|
cwd: policy.cwd || orchestrator.baseDir,
|
|
563
564
|
workspaceMode: policy.workspaceMode ?? "inherit",
|
|
564
565
|
label,
|
package/src/config-store.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
SpawnPolicy,
|
|
13
13
|
SpawnProvider,
|
|
14
14
|
StewardConfig,
|
|
15
|
+
WorkspaceConfig,
|
|
15
16
|
} from "./types";
|
|
16
17
|
|
|
17
18
|
const CONFIG_HISTORY_LIMIT = 50;
|
|
@@ -21,6 +22,8 @@ const STEWARD_NAMESPACE = "steward";
|
|
|
21
22
|
const STEWARD_KEY = "default";
|
|
22
23
|
const INSIGHTS_NAMESPACE = "insights";
|
|
23
24
|
const INSIGHTS_KEY = "default";
|
|
25
|
+
const WORKSPACE_NAMESPACE = "workspace";
|
|
26
|
+
const WORKSPACE_KEY = "default";
|
|
24
27
|
const VALID_PROVIDERS = ["claude", "codex"] as const;
|
|
25
28
|
const VALID_PROFILE_PROVIDERS = ["any", "claude", "codex"] as const;
|
|
26
29
|
const VALID_PROFILE_BASES = ["host", "minimal", "isolated"] as const;
|
|
@@ -472,12 +475,34 @@ function validateInsightsConfig(value: unknown): InsightsConfig {
|
|
|
472
475
|
};
|
|
473
476
|
}
|
|
474
477
|
|
|
478
|
+
// Global workspace provisioning config for isolated worktrees (#159 follow-up).
|
|
479
|
+
// Defaults seed the two untracked paths an isolated agent almost always needs:
|
|
480
|
+
// the agent guide and the rig config, both gitignored so a fresh worktree lacks them.
|
|
481
|
+
const WORKSPACE_CONFIG_DEFAULTS: WorkspaceConfig = {
|
|
482
|
+
symlinkPaths: ["AGENTS.md", ".claude-rig"],
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
function validateWorkspaceConfig(value: unknown): WorkspaceConfig {
|
|
486
|
+
if (!isRecord(value)) throw new ValidationError("workspace config value must be an object");
|
|
487
|
+
const symlinkPaths = cleanStringArray(value.symlinkPaths, "symlinkPaths");
|
|
488
|
+
// Reject absolute paths and parent-traversal up front: symlink sources must stay
|
|
489
|
+
// inside the main checkout. The orchestrator re-checks containment at link time,
|
|
490
|
+
// but failing here gives the operator immediate feedback in the dashboard.
|
|
491
|
+
for (const entry of symlinkPaths) {
|
|
492
|
+
if (entry.startsWith("/") || entry.split(/[\\/]/).includes("..")) {
|
|
493
|
+
throw new ValidationError(`symlinkPaths entry must be a relative path within the repo: ${entry}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return { symlinkPaths };
|
|
497
|
+
}
|
|
498
|
+
|
|
475
499
|
function normalizeValue(namespace: string, key: string, value: unknown): unknown {
|
|
476
500
|
if (value === undefined) throw new ValidationError("value required");
|
|
477
501
|
if (namespace === SPAWN_POLICY_NAMESPACE) return validateSpawnPolicy(key, value);
|
|
478
502
|
if (namespace === AGENT_PROFILE_NAMESPACE) return validateAgentProfile(key, value);
|
|
479
503
|
if (namespace === STEWARD_NAMESPACE) return validateStewardConfig(value);
|
|
480
504
|
if (namespace === INSIGHTS_NAMESPACE) return validateInsightsConfig(value);
|
|
505
|
+
if (namespace === WORKSPACE_NAMESPACE) return validateWorkspaceConfig(value);
|
|
481
506
|
if (JSON.stringify(value) === undefined) throw new ValidationError("value must be valid JSON");
|
|
482
507
|
return value;
|
|
483
508
|
}
|
|
@@ -614,6 +639,40 @@ export function setInsightsConfig(value: unknown, updatedBy?: string): ConfigEnt
|
|
|
614
639
|
return setConfig(INSIGHTS_NAMESPACE, INSIGHTS_KEY, value as InsightsConfig, updatedBy);
|
|
615
640
|
}
|
|
616
641
|
|
|
642
|
+
/** Global workspace config, merged over defaults (always returns a usable value). */
|
|
643
|
+
export function getWorkspaceConfig(): WorkspaceConfig {
|
|
644
|
+
const entry = getConfig<Partial<WorkspaceConfig>>(WORKSPACE_NAMESPACE, WORKSPACE_KEY);
|
|
645
|
+
if (!entry) return { ...WORKSPACE_CONFIG_DEFAULTS };
|
|
646
|
+
return validateWorkspaceConfig({ ...WORKSPACE_CONFIG_DEFAULTS, ...entry.value });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function getWorkspaceConfigEntry(): ConfigEntry<WorkspaceConfig> {
|
|
650
|
+
const entry = getConfig<WorkspaceConfig>(WORKSPACE_NAMESPACE, WORKSPACE_KEY);
|
|
651
|
+
return entry ?? {
|
|
652
|
+
namespace: WORKSPACE_NAMESPACE,
|
|
653
|
+
key: WORKSPACE_KEY,
|
|
654
|
+
value: { ...WORKSPACE_CONFIG_DEFAULTS },
|
|
655
|
+
version: 0,
|
|
656
|
+
updatedAt: "default",
|
|
657
|
+
updatedBy: "system",
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function setWorkspaceConfig(value: unknown, updatedBy?: string): ConfigEntry<WorkspaceConfig> {
|
|
662
|
+
return setConfig(WORKSPACE_NAMESPACE, WORKSPACE_KEY, value as WorkspaceConfig, updatedBy);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Spawn-param fragment carrying the global workspace symlink list to the orchestrator.
|
|
667
|
+
* Spread into every spawn command's params next to `agentProfile` so any isolated
|
|
668
|
+
* worktree — direct spawn, managed policy, restart, automation — provisions the same
|
|
669
|
+
* untracked paths. Returns `{}` when nothing is configured (keeps params lean).
|
|
670
|
+
*/
|
|
671
|
+
export function workspaceSpawnParams(): { workspaceSymlinks?: string[] } {
|
|
672
|
+
const { symlinkPaths } = getWorkspaceConfig();
|
|
673
|
+
return symlinkPaths.length ? { workspaceSymlinks: symlinkPaths } : {};
|
|
674
|
+
}
|
|
675
|
+
|
|
617
676
|
function builtInProfileEntry(profile: AgentProfile): ConfigEntry<AgentProfile> {
|
|
618
677
|
return {
|
|
619
678
|
namespace: AGENT_PROFILE_NAMESPACE,
|
package/src/db.ts
CHANGED
|
@@ -239,7 +239,8 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
239
239
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
240
240
|
meta TEXT NOT NULL DEFAULT '{}',
|
|
241
241
|
read_by TEXT NOT NULL DEFAULT '[]',
|
|
242
|
-
created_at INTEGER NOT NULL
|
|
242
|
+
created_at INTEGER NOT NULL,
|
|
243
|
+
occurred_at INTEGER
|
|
243
244
|
);
|
|
244
245
|
|
|
245
246
|
CREATE INDEX IF NOT EXISTS idx_msg_to ON messages(to_target);
|
|
@@ -890,6 +891,11 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
890
891
|
if (!colNames.includes("resolved_to_agent")) {
|
|
891
892
|
db.run("ALTER TABLE messages ADD COLUMN resolved_to_agent TEXT");
|
|
892
893
|
}
|
|
894
|
+
// Event time (#196): when a Runner queues a message in its durable outbox during an
|
|
895
|
+
// outage, occurred_at preserves when it really happened vs. the later receive time.
|
|
896
|
+
if (!colNames.includes("occurred_at")) {
|
|
897
|
+
db.run("ALTER TABLE messages ADD COLUMN occurred_at INTEGER");
|
|
898
|
+
}
|
|
893
899
|
db.query(
|
|
894
900
|
"UPDATE messages SET claim_expires_at = coalesce(claimed_at, ?) + ? WHERE claimed_by IS NOT NULL AND claim_expires_at IS NULL",
|
|
895
901
|
).run(Date.now(), CLAIM_LEASE_MS);
|
|
@@ -1308,6 +1314,7 @@ function rowToMessage(row: any): Message {
|
|
|
1308
1314
|
reactions: parseJson(row.reactions_json ?? "[]", []),
|
|
1309
1315
|
readBy: parseJson(row.read_by_agents ?? "[]", []),
|
|
1310
1316
|
createdAt: row.created_at,
|
|
1317
|
+
occurredAt: row.occurred_at ?? undefined,
|
|
1311
1318
|
};
|
|
1312
1319
|
}
|
|
1313
1320
|
|
|
@@ -3680,6 +3687,16 @@ function findRecentDuplicateReply(input: SendMessageInput, threadId: number | nu
|
|
|
3680
3687
|
return row ? rowToMessage(row) : null;
|
|
3681
3688
|
}
|
|
3682
3689
|
|
|
3690
|
+
// Event time may be queued-then-backfilled, so it can legitimately be older than the
|
|
3691
|
+
// receive time — but it must be a sane epoch-ms value. Returns null (column stays NULL, so
|
|
3692
|
+
// readers fall back to created_at) for absent/invalid values or anything more than a minute
|
|
3693
|
+
// in the future (clock-skew guard). Only a real backfilled time is stored.
|
|
3694
|
+
function sanitizeOccurredAt(occurredAt: number | undefined, receivedAt: number): number | null {
|
|
3695
|
+
if (typeof occurredAt !== "number" || !Number.isFinite(occurredAt)) return null;
|
|
3696
|
+
if (occurredAt <= 0 || occurredAt > receivedAt + 60_000) return null;
|
|
3697
|
+
return Math.floor(occurredAt);
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3683
3700
|
export function sendMessageWithResult(input: SendMessageInput): { message: Message; created: boolean } {
|
|
3684
3701
|
const now = Date.now();
|
|
3685
3702
|
const payload = input.payload ?? {};
|
|
@@ -3710,12 +3727,12 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
3710
3727
|
INSERT INTO messages (
|
|
3711
3728
|
from_agent, to_target, kind, channel, subject, body, thread_id, reply_to, claimable,
|
|
3712
3729
|
idempotency_key, delivery_status, queued_at, max_age_seconds, resolved_to_agent,
|
|
3713
|
-
payload, meta, created_at
|
|
3730
|
+
payload, meta, created_at, occurred_at
|
|
3714
3731
|
)
|
|
3715
3732
|
VALUES (
|
|
3716
3733
|
$from, $to, $kind, $channel, $subject, $body, $threadId, $replyTo, $claimable,
|
|
3717
3734
|
$idempotencyKey, $deliveryStatus, $queuedAt, $maxAgeSeconds, $resolvedToAgent,
|
|
3718
|
-
$payload, $meta, $now
|
|
3735
|
+
$payload, $meta, $now, $occurredAt
|
|
3719
3736
|
)
|
|
3720
3737
|
`);
|
|
3721
3738
|
const setSelfThread = db.query("UPDATE messages SET thread_id = ? WHERE id = ?");
|
|
@@ -3756,6 +3773,8 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
3756
3773
|
$payload: JSON.stringify(payload),
|
|
3757
3774
|
$meta: JSON.stringify(input.meta ?? {}),
|
|
3758
3775
|
$now: now,
|
|
3776
|
+
// Sanitize: only accept a plausible epoch-ms event time, else fall back to receive time.
|
|
3777
|
+
$occurredAt: sanitizeOccurredAt(input.occurredAt, now),
|
|
3759
3778
|
});
|
|
3760
3779
|
const newId = Number(result.lastInsertRowid);
|
|
3761
3780
|
if (threadId === null) setSelfThread.run(newId, newId);
|
package/src/managed-policy.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveProviderSelection } from "agent-relay-sdk/provider-catalog";
|
|
2
|
-
import { getAgentProfile } from "./config-store";
|
|
2
|
+
import { getAgentProfile, workspaceSpawnParams } from "./config-store";
|
|
3
3
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
4
4
|
import type { SpawnPolicy, WorkspaceMode } from "./types";
|
|
5
5
|
|
|
@@ -55,6 +55,7 @@ export function buildManagedSpawnParams(policy: SpawnPolicy, requestId: string,
|
|
|
55
55
|
...resolvedModelParams(policy),
|
|
56
56
|
...(policy.profile ? { profile: policy.profile } : {}),
|
|
57
57
|
...(agentProfile ? { agentProfile } : {}),
|
|
58
|
+
...workspaceSpawnParams(),
|
|
58
59
|
label: policy.label,
|
|
59
60
|
tags: policy.tags,
|
|
60
61
|
capabilities: policy.capabilities,
|
package/src/routes.ts
CHANGED
|
@@ -108,6 +108,9 @@ import {
|
|
|
108
108
|
getStewardConfigEntry,
|
|
109
109
|
getInsightsConfigEntry,
|
|
110
110
|
setInsightsConfig,
|
|
111
|
+
getWorkspaceConfigEntry,
|
|
112
|
+
setWorkspaceConfig,
|
|
113
|
+
workspaceSpawnParams,
|
|
111
114
|
listAgentProfiles,
|
|
112
115
|
listSpawnPolicies,
|
|
113
116
|
listConfig,
|
|
@@ -594,6 +597,12 @@ function normalizeMessageInput(body: unknown): SendMessageInput {
|
|
|
594
597
|
}
|
|
595
598
|
input.maxAgeSeconds = body.maxAgeSeconds;
|
|
596
599
|
}
|
|
600
|
+
if (body.occurredAt !== undefined) {
|
|
601
|
+
if (typeof body.occurredAt !== "number" || !Number.isFinite(body.occurredAt) || body.occurredAt <= 0) {
|
|
602
|
+
throw new ValidationError("occurredAt must be a positive epoch-ms number");
|
|
603
|
+
}
|
|
604
|
+
input.occurredAt = body.occurredAt;
|
|
605
|
+
}
|
|
597
606
|
|
|
598
607
|
const channel = cleanString(body.channel, "channel", { max: 120 });
|
|
599
608
|
if (channel) input.channel = channel;
|
|
@@ -2296,6 +2305,7 @@ function restartSpawnParamsForAgent(
|
|
|
2296
2305
|
workspaceMode,
|
|
2297
2306
|
...(profileName ? { profile: profileName } : {}),
|
|
2298
2307
|
...(agentProfile ? { agentProfile } : {}),
|
|
2308
|
+
...workspaceSpawnParams(),
|
|
2299
2309
|
...(label ? { label } : {}),
|
|
2300
2310
|
agentId: agent.id,
|
|
2301
2311
|
tags: agent.tags,
|
|
@@ -3612,6 +3622,7 @@ const postOrchestratorSpawn: Handler = async (req, params) => {
|
|
|
3612
3622
|
permissionMode: approvalMode,
|
|
3613
3623
|
profile,
|
|
3614
3624
|
agentProfile,
|
|
3625
|
+
...workspaceSpawnParams(),
|
|
3615
3626
|
providerArgs,
|
|
3616
3627
|
prompt,
|
|
3617
3628
|
systemPromptAppend,
|
|
@@ -4599,6 +4610,25 @@ const putStewardConfigRoute: Handler = async (req) => {
|
|
|
4599
4610
|
}
|
|
4600
4611
|
};
|
|
4601
4612
|
|
|
4613
|
+
const getWorkspaceConfigRoute: Handler = () => json(getWorkspaceConfigEntry());
|
|
4614
|
+
|
|
4615
|
+
const putWorkspaceConfigRoute: Handler = async (req) => {
|
|
4616
|
+
const parsed = await parseBody<unknown>(req);
|
|
4617
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
4618
|
+
try {
|
|
4619
|
+
const value = isRecord(parsed.body) && Object.prototype.hasOwnProperty.call(parsed.body, "value")
|
|
4620
|
+
? parsed.body.value
|
|
4621
|
+
: parsed.body;
|
|
4622
|
+
const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
|
|
4623
|
+
const entry = setWorkspaceConfig(value, updatedBy);
|
|
4624
|
+
emitConfigChanged(entry.namespace, entry.key, entry.version);
|
|
4625
|
+
return json(entry, entry.version === 1 ? 201 : 200);
|
|
4626
|
+
} catch (e) {
|
|
4627
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
4628
|
+
throw e;
|
|
4629
|
+
}
|
|
4630
|
+
};
|
|
4631
|
+
|
|
4602
4632
|
// --- Insights / self-improvement (epic #183, docs/self-improvement.md) ---
|
|
4603
4633
|
|
|
4604
4634
|
const getInsightsConfigRoute: Handler = () => json(getInsightsConfigEntry());
|
|
@@ -4635,6 +4665,14 @@ const getInsightsObservationsRoute: Handler = (req) => {
|
|
|
4635
4665
|
});
|
|
4636
4666
|
};
|
|
4637
4667
|
|
|
4668
|
+
// Accept a backfilled event time only when it's a sane epoch-ms value within a minute of
|
|
4669
|
+
// now (clock-skew guard); otherwise let recordObservation default to receive time.
|
|
4670
|
+
function sanitizeObservationOccurredAt(occurredAt: unknown): number | undefined {
|
|
4671
|
+
if (typeof occurredAt !== "number" || !Number.isFinite(occurredAt)) return undefined;
|
|
4672
|
+
if (occurredAt <= 0 || occurredAt > Date.now() + 60_000) return undefined;
|
|
4673
|
+
return Math.floor(occurredAt);
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4638
4676
|
const postInsightsObservationRoute: Handler = async (req) => {
|
|
4639
4677
|
const parsed = await parseBody<unknown>(req);
|
|
4640
4678
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
@@ -4656,6 +4694,10 @@ const postInsightsObservationRoute: Handler = async (req) => {
|
|
|
4656
4694
|
value: isRecord(body.value) ? body.value : {},
|
|
4657
4695
|
outcome: isRecord(body.outcome) ? body.outcome : undefined,
|
|
4658
4696
|
source: body.source === "server" ? "server" : "agent",
|
|
4697
|
+
// For insights the event time IS the record time (end-of-session). When the Runner
|
|
4698
|
+
// backfilled this through its durable outbox (#196), occurredAt preserves the real
|
|
4699
|
+
// moment rather than the later server-receive time.
|
|
4700
|
+
createdAt: sanitizeObservationOccurredAt(body.occurredAt),
|
|
4659
4701
|
});
|
|
4660
4702
|
return json(observation, 201);
|
|
4661
4703
|
} catch (e) {
|
|
@@ -6569,6 +6611,8 @@ const routes: Route[] = [
|
|
|
6569
6611
|
route("DELETE", "/api/agent-profiles/:name", deleteAgentProfileRoute),
|
|
6570
6612
|
route("GET", "/api/steward-config", getStewardConfigRoute),
|
|
6571
6613
|
route("PUT", "/api/steward-config", putStewardConfigRoute),
|
|
6614
|
+
route("GET", "/api/workspace-config", getWorkspaceConfigRoute),
|
|
6615
|
+
route("PUT", "/api/workspace-config", putWorkspaceConfigRoute),
|
|
6572
6616
|
route("GET", "/api/insights/config", getInsightsConfigRoute),
|
|
6573
6617
|
route("PUT", "/api/insights/config", putInsightsConfigRoute),
|
|
6574
6618
|
route("GET", "/api/insights/observations", getInsightsObservationsRoute),
|