@north-light/crouter 0.3.220 → 0.3.222
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/dist/api/client.d.ts +29 -0
- package/dist/api/client.js +44 -0
- package/dist/api/dto/chat-inventory.d.ts +56 -0
- package/dist/api/dto/chat-inventory.js +11 -0
- package/dist/api/dto/human-requests.d.ts +88 -0
- package/dist/api/dto/human-requests.js +4 -0
- package/dist/api/dto/human.d.ts +3 -0
- package/dist/api/dto/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/dto/reviews.d.ts +2 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/routes.d.ts +8 -0
- package/dist/api/routes.js +11 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
- package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
- package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
- package/dist/builtin-memory/04-base-worker.md +4 -8
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
- package/dist/builtin-memory/design/guide.md +35 -0
- package/dist/builtin-memory/design/roadmap.md +21 -0
- package/dist/builtin-memory/insights/capture.md +1 -1
- package/dist/builtin-memory/internal/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- package/dist/builtin-memory/internal/plugins.md +10 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/plan/roadmap.md +6 -22
- package/dist/builtin-memory/spec/guide.md +23 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +34 -16
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
- package/dist/clients/attach/overlays/file-review.js +2 -2
- package/dist/clients/attach/render/markdown-source.js +106 -1
- package/dist/clients/attach/session/file-links.d.ts +13 -4
- package/dist/clients/attach/session/file-links.js +54 -58
- package/dist/clients/attach/session/keys.d.ts +1 -1
- package/dist/clients/attach/session/profile-files.js +1 -1
- package/dist/clients/attach/viewer.js +698 -696
- package/dist/clients/inbox/controller.js +1 -1
- package/dist/clients/inbox/resolve.d.ts +1 -0
- package/dist/clients/inbox/review/launch.d.ts +8 -4
- package/dist/clients/inbox/review/launch.js +55 -5
- package/dist/clients/inbox/review/review-client.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +7 -1
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- package/dist/commands/__tests__/human.test.js +2 -2
- package/dist/commands/human/request.d.ts +2 -0
- package/dist/commands/human/request.js +281 -0
- package/dist/commands/human.js +5 -2
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/read.js +1 -0
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.js +165 -75
- package/dist/commands/pkg/plugin-inspect.js +19 -2
- package/dist/commands/pkg/plugin-manage.d.ts +8 -3
- package/dist/commands/pkg/plugin-manage.js +72 -24
- package/dist/commands/profile/default.js +6 -10
- package/dist/commands/profile/list.js +5 -3
- package/dist/commands/profile/new.js +21 -8
- package/dist/commands/profile/project.js +25 -19
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/surface-inbox.js +1 -0
- package/dist/commands/sys/__tests__/migrate.test.js +16 -5
- package/dist/commands/sys/config.js +2 -2
- package/dist/commands/sys/doctor.js +87 -5
- package/dist/commands/sys/migrate.js +38 -19
- package/dist/commands/sys/setup-core.js +1 -1
- package/dist/commands/sys/sync-project-guidance.js +1 -1
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
- package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
- package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/human-action-delivery.test.js +140 -0
- package/dist/core/__tests__/human-actions.test.d.ts +1 -0
- package/dist/core/__tests__/human-actions.test.js +116 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
- package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
- package/dist/core/bootstrap.js +6 -0
- package/dist/core/canvas/browse/app.js +5 -2
- package/dist/core/canvas/browse/model.d.ts +25 -15
- package/dist/core/canvas/browse/model.js +86 -65
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/canvas/human-deliveries.d.ts +53 -0
- package/dist/core/canvas/human-deliveries.js +75 -0
- package/dist/core/canvas/render-source.d.ts +6 -0
- package/dist/core/canvas/render-source.js +7 -1
- package/dist/core/canvas/render.js +10 -2
- package/dist/core/command-hooks/artifact.d.ts +10 -0
- package/dist/core/command-hooks/artifact.js +129 -0
- package/dist/core/command-hooks/catalog.d.ts +14 -0
- package/dist/core/command-hooks/catalog.js +38 -0
- package/dist/core/command-hooks/compose.d.ts +15 -0
- package/dist/core/command-hooks/compose.js +99 -0
- package/dist/core/command-hooks/discovery.d.ts +87 -0
- package/dist/core/command-hooks/discovery.js +174 -0
- package/dist/core/command-hooks/help.d.ts +5 -0
- package/dist/core/command-hooks/help.js +18 -0
- package/dist/core/command-hooks/index.d.ts +6 -0
- package/dist/core/command-hooks/index.js +6 -0
- package/dist/core/command-hooks/report.d.ts +23 -0
- package/dist/core/command-hooks/report.js +19 -0
- package/dist/core/command-hooks/schema.d.ts +27 -0
- package/dist/core/command-hooks/schema.js +68 -0
- package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
- package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
- package/dist/core/command-plugins/presence.d.ts +2 -0
- package/dist/core/command-plugins/presence.js +17 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
- package/dist/core/command.d.ts +8 -1
- package/dist/core/command.js +12 -10
- package/dist/core/config.d.ts +13 -1
- package/dist/core/config.js +51 -1
- package/dist/core/feed/inbox.d.ts +6 -0
- package/dist/core/feed/inbox.js +9 -1
- package/dist/core/help.d.ts +7 -1
- package/dist/core/human/action-binding.d.ts +21 -0
- package/dist/core/human/action-binding.js +40 -0
- package/dist/core/human/completion.d.ts +38 -0
- package/dist/core/human/completion.js +27 -0
- package/dist/core/human/convention.d.ts +2 -0
- package/dist/core/human/convention.js +2 -0
- package/dist/core/human/tickets.d.ts +25 -6
- package/dist/core/human/tickets.js +19 -13
- package/dist/core/human/types.d.ts +5 -0
- package/dist/core/human-actions.d.ts +25 -0
- package/dist/core/human-actions.js +101 -0
- package/dist/core/io.d.ts +9 -1
- package/dist/core/io.js +44 -2
- package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
- package/dist/core/memory/inline-ref-inventory.js +15 -8
- package/dist/core/memory-resolver.d.ts +13 -1
- package/dist/core/memory-resolver.js +26 -20
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.d.ts +2 -0
- package/dist/core/profiles/select.js +29 -12
- package/dist/core/render.js +11 -0
- package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
- package/dist/core/runtime/advertised-command-invocation.js +233 -0
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/broker/event-projection.js +7 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +13 -11
- package/dist/core/runtime/broker/read-ops.d.ts +4 -0
- package/dist/core/runtime/broker/read-ops.js +6 -2
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-inventory.d.ts +4 -0
- package/dist/core/runtime/broker-inventory.js +116 -0
- package/dist/core/runtime/broker-persona-guidance.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -2
- package/dist/core/runtime/broker.js +10 -1
- package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
- package/dist/core/runtime/chat-inventory-rows.js +105 -0
- package/dist/core/runtime/command-surface.d.ts +38 -0
- package/dist/core/runtime/command-surface.js +117 -0
- package/dist/core/runtime/launch-target.d.ts +25 -0
- package/dist/core/runtime/launch-target.js +54 -0
- package/dist/core/runtime/node-read.js +5 -0
- package/dist/core/runtime/persona.js +3 -3
- package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
- package/dist/core/runtime/prospective-inventory-cli.js +61 -0
- package/dist/core/runtime/prospective-inventory.d.ts +10 -0
- package/dist/core/runtime/prospective-inventory.js +88 -0
- package/dist/core/runtime/spawn.d.ts +3 -1
- package/dist/core/runtime/spawn.js +5 -3
- package/dist/core/scope.d.ts +26 -1
- package/dist/core/scope.js +52 -12
- package/dist/core/substrate/on-read.d.ts +7 -1
- package/dist/core/substrate/on-read.js +30 -32
- package/dist/core/substrate/render-node.d.ts +3 -2
- package/dist/core/substrate/render-node.js +3 -2
- package/dist/core/substrate/render.js +65 -24
- package/dist/core/substrate/schema.d.ts +16 -2
- package/dist/core/substrate/schema.js +14 -5
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +56 -7
- package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/chat-inventory.js +25 -0
- package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
- package/dist/daemon/api/handlers/human-requests.js +409 -0
- package/dist/daemon/api/handlers/human.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +3 -0
- package/dist/daemon/api/handlers/nodes.d.ts +1 -3
- package/dist/daemon/api/handlers/nodes.js +11 -46
- package/dist/daemon/api/handlers/profiles.js +7 -1
- package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
- package/dist/daemon/api/handlers/reviews.js +10 -2
- package/dist/daemon/api/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +6 -0
- package/dist/daemon/crtrd.js +6 -0
- package/dist/daemon/human/deliver-action.d.ts +16 -0
- package/dist/daemon/human/deliver-action.js +168 -0
- package/dist/daemon/human/finish.d.ts +8 -5
- package/dist/daemon/human/finish.js +45 -6
- package/dist/daemon/human/sweep.js +4 -1
- package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
- package/dist/daemon/review/finish.d.ts +8 -3
- package/dist/daemon/review/finish.js +19 -1
- package/dist/hook-authoring.d.ts +75 -0
- package/dist/hook-authoring.js +358 -0
- package/dist/hook-process.d.ts +7 -0
- package/dist/hook-process.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/migrations/002-profile-project-memory.d.ts +2 -0
- package/dist/migrations/002-profile-project-memory.js +71 -0
- package/dist/migrations/profile-manifests.d.ts +30 -0
- package/dist/migrations/profile-manifests.js +70 -0
- package/dist/migrations/registry.js +10 -5
- package/dist/migrations/types.d.ts +28 -1
- package/dist/migrations/types.js +15 -9
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
- package/dist/pi-extensions/canvas-structured-output.js +85 -2
- package/dist/types.d.ts +23 -6
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/00-runtime-base.md +0 -55
- package/dist/builtin-memory/design.md +0 -55
- package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/fixtures/memory-slash-live-probe.d.ts} +0 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { after, afterEach, beforeEach, test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { createNode, getNode, listNodes } from '../../../core/canvas/canvas.js';
|
|
7
7
|
import { closeDb } from '../../../core/canvas/db.js';
|
|
8
8
|
import { CrtrError } from '../../../core/errors.js';
|
|
9
|
-
import { createProfile, loadProfileManifest, pauseProfile, setProfileDefaultKind } from '../../../core/profiles/manifest.js';
|
|
9
|
+
import { createProfile, loadProfileManifest, pauseProfile, profileManifestPath, ROOT_PROFILE_ID, setProfileDefaultKind } from '../../../core/profiles/manifest.js';
|
|
10
10
|
import { resetScopeCache } from '../../../core/scope.js';
|
|
11
11
|
import { reviveNode } from '../../../core/runtime/revive.js';
|
|
12
|
+
import { resolveLaunchTarget } from '../../../core/runtime/launch-target.js';
|
|
12
13
|
import { spawnNode } from '../../../core/runtime/nodes.js';
|
|
13
|
-
import { handleCreate, resolveCreateKind } from '../handlers/nodes.js';
|
|
14
|
+
import { handleConfig, handleCreate, resolveCreateKind } from '../handlers/nodes.js';
|
|
14
15
|
const previousCrtrHome = process.env['CRTR_HOME'];
|
|
15
16
|
const previousHome = process.env.HOME;
|
|
16
17
|
let home = '';
|
|
@@ -42,7 +43,7 @@ after(() => {
|
|
|
42
43
|
resetScopeCache();
|
|
43
44
|
});
|
|
44
45
|
test('create rejects a paused profile before a node exists', async () => {
|
|
45
|
-
const profile = createProfile('paused profile', [cwd]);
|
|
46
|
+
const profile = createProfile('paused profile', [{ path: cwd, memory: 'content' }]);
|
|
46
47
|
pauseProfile(profile.profileId);
|
|
47
48
|
const ctx = {
|
|
48
49
|
method: 'POST',
|
|
@@ -62,7 +63,7 @@ test('create rejects a paused profile before a node exists', async () => {
|
|
|
62
63
|
assert.equal(listNodes().length, beforeRows);
|
|
63
64
|
});
|
|
64
65
|
test('revive refuses a paused profile before launching a broker', () => {
|
|
65
|
-
const profile = createProfile('paused revive', [cwd]);
|
|
66
|
+
const profile = createProfile('paused revive', [{ path: cwd, memory: 'content' }]);
|
|
66
67
|
pauseProfile(profile.profileId);
|
|
67
68
|
const nodeId = 'paused-revive-node';
|
|
68
69
|
createNode({
|
|
@@ -90,7 +91,7 @@ test('revive refuses a paused profile before launching a broker', () => {
|
|
|
90
91
|
// because they all reach the canvas through spawnNode and none of them call
|
|
91
92
|
// handleCreate.
|
|
92
93
|
test('the birth gate refuses a paused profile for a daemon-side spawn', () => {
|
|
93
|
-
const profile = createProfile('paused spawn', [cwd]);
|
|
94
|
+
const profile = createProfile('paused spawn', [{ path: cwd, memory: 'content' }]);
|
|
94
95
|
pauseProfile(profile.profileId);
|
|
95
96
|
const beforeRows = listNodes().length;
|
|
96
97
|
assert.throws(() => spawnNode({ kind: 'general', cwd, parent: null, profile_id: profile.profileId }), (error) => {
|
|
@@ -101,8 +102,56 @@ test('the birth gate refuses a paused profile for a daemon-side spawn', () => {
|
|
|
101
102
|
});
|
|
102
103
|
assert.equal(listNodes().length, beforeRows);
|
|
103
104
|
});
|
|
105
|
+
test('prospective root selection stays read-only before the root profile is scaffolded', async () => {
|
|
106
|
+
const manifestPath = profileManifestPath(ROOT_PROFILE_ID);
|
|
107
|
+
assert.equal(existsSync(manifestPath), false);
|
|
108
|
+
const target = await resolveLaunchTarget({ root: true, cwd }, { readOnlyProfileSelection: true });
|
|
109
|
+
assert.equal(target.profileId, ROOT_PROFILE_ID);
|
|
110
|
+
assert.equal(target.profile, null);
|
|
111
|
+
assert.equal(target.kind, 'general');
|
|
112
|
+
assert.equal(target.cwd, cwd);
|
|
113
|
+
assert.equal(existsSync(manifestPath), false);
|
|
114
|
+
});
|
|
115
|
+
test('target-scoped prospective validation does not rescope node writes', async () => {
|
|
116
|
+
mkdirSync(join(cwd, '.crouter'), { recursive: true });
|
|
117
|
+
writeFileSync(join(cwd, '.crouter', 'config.json'), JSON.stringify({
|
|
118
|
+
kinds: { 'target-only': { whenToUse: 'Only in this project.' } },
|
|
119
|
+
}));
|
|
120
|
+
const target = await resolveLaunchTarget({ root: true, cwd, kind: 'target-only' }, { readOnlyProfileSelection: true });
|
|
121
|
+
assert.equal(target.kind, 'target-only');
|
|
122
|
+
const create = {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
path: '/v1/nodes',
|
|
125
|
+
params: {},
|
|
126
|
+
query: new URLSearchParams(),
|
|
127
|
+
body: { root: true, profile: null, cwd, kind: 'target-only' },
|
|
128
|
+
};
|
|
129
|
+
await assert.rejects(() => handleCreate(create), /unknown kind: target-only/);
|
|
130
|
+
const id = 'ambient-kind-write';
|
|
131
|
+
createNode({
|
|
132
|
+
node_id: id,
|
|
133
|
+
name: id,
|
|
134
|
+
created: new Date().toISOString(),
|
|
135
|
+
cwd,
|
|
136
|
+
kind: 'general',
|
|
137
|
+
mode: 'base',
|
|
138
|
+
lifecycle: 'terminal',
|
|
139
|
+
status: 'active',
|
|
140
|
+
parent: null,
|
|
141
|
+
profile_id: null,
|
|
142
|
+
});
|
|
143
|
+
const patch = {
|
|
144
|
+
method: 'PATCH',
|
|
145
|
+
path: `/v1/nodes/${id}/config`,
|
|
146
|
+
params: { id },
|
|
147
|
+
query: new URLSearchParams(),
|
|
148
|
+
body: { kind: 'target-only' },
|
|
149
|
+
};
|
|
150
|
+
await assert.rejects(() => handleConfig(patch), /unknown kind: target-only/);
|
|
151
|
+
assert.equal(getNode(id)?.kind, 'general');
|
|
152
|
+
});
|
|
104
153
|
test('create kind resolves from the profile default when omitted', () => {
|
|
105
|
-
const profile = createProfile('default kind', [cwd]);
|
|
154
|
+
const profile = createProfile('default kind', [{ path: cwd, memory: 'content' }]);
|
|
106
155
|
setProfileDefaultKind(profile.profileId, 'developer');
|
|
107
156
|
const entry = loadProfileManifest(profile.profileId);
|
|
108
157
|
assert.equal(resolveCreateKind(undefined, entry), 'developer');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Chat-inventory handler. `GET /v1/nodes/{id}/chat-inventory` answers what a
|
|
2
|
+
// non-terminal chat surface may offer for one node: the slash commands its live
|
|
3
|
+
// engine registered that opted into chat disclosure, and the memory documents an
|
|
4
|
+
// inline `/name` token resolves to.
|
|
5
|
+
//
|
|
6
|
+
// Read-only in both directions — it takes one observer round trip against a
|
|
7
|
+
// broker that is already running and writes no canvas state. A node whose broker
|
|
8
|
+
// is not live answers 200 with `broker_live: false` and empty arrays rather than
|
|
9
|
+
// 404 or a revival: "this node has nothing to offer right now" is a normal
|
|
10
|
+
// answer, not an error.
|
|
11
|
+
import { getNode } from '../../../core/canvas/canvas.js';
|
|
12
|
+
import { isSafeNodeId } from '../../../core/canvas/paths.js';
|
|
13
|
+
import { notFound, usage } from '../../../core/errors.js';
|
|
14
|
+
import { readChatInventory } from '../../../core/runtime/broker-inventory.js';
|
|
15
|
+
async function handleGet(ctx) {
|
|
16
|
+
const id = ctx.params['id'];
|
|
17
|
+
if (!isSafeNodeId(id))
|
|
18
|
+
throw usage(`invalid node id: ${id}`, { received: id });
|
|
19
|
+
if (getNode(id) === null)
|
|
20
|
+
throw notFound(`unknown node: ${id}`, { received: id });
|
|
21
|
+
return { status: 200, body: await readChatInventory(id) };
|
|
22
|
+
}
|
|
23
|
+
export const chatInventoryRoutes = [
|
|
24
|
+
{ method: 'GET', pattern: '/v1/nodes/:id/chat-inventory', handler: handleGet },
|
|
25
|
+
];
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// Durable programmatic human requests — crtrd `/v1/human/requests`. A request
|
|
2
|
+
// is an ordinary page ticket with an optional frozen completion action, so it
|
|
3
|
+
// is the same record the inbox lists, answers, and dismisses. Creation is the
|
|
4
|
+
// only surface that writes the action binding; every settlement path funnels
|
|
5
|
+
// through the shared first-writer-wins store.
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { statSync } from 'node:fs';
|
|
8
|
+
import { basename, isAbsolute } from 'node:path';
|
|
9
|
+
import { ApiError } from '../../../api/index.js';
|
|
10
|
+
import { readActionDelivery } from '../../../core/canvas/human-deliveries.js';
|
|
11
|
+
import { resolvePageComponents } from '../../../core/config.js';
|
|
12
|
+
import { resolveHumanAction } from '../../../core/human-actions.js';
|
|
13
|
+
import { HUMAN_ACTION_NAME_PATTERN, publishTicketActionBinding, readTicketActionBinding, } from '../../../core/human/action-binding.js';
|
|
14
|
+
import { completionEventFor } from '../../../core/human/completion.js';
|
|
15
|
+
import { parsePage } from '../../../core/human/page.js';
|
|
16
|
+
import { opaqueInboxTicketId, ticketDir } from '../../../core/human/root.js';
|
|
17
|
+
import { preparePageSource, publishPage, readPageTicket, readTicketResult, validatePageTicketResponses, } from '../../../core/human/tickets.js';
|
|
18
|
+
import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
|
|
19
|
+
import { resolveTicket, requireTicketId } from './inbox.js';
|
|
20
|
+
const REASON_MAX = 1000;
|
|
21
|
+
const ACTOR_MAX = 200;
|
|
22
|
+
const DEFAULT_DELIVERY = { placement: 'inline', inbox: true, reply: false };
|
|
23
|
+
const SOURCE_FIELDS = ['sessionName', 'askedBy', 'emittedAt', 'profileName', 'nodeId'];
|
|
24
|
+
// ===========================================================================
|
|
25
|
+
// Body validation
|
|
26
|
+
// ===========================================================================
|
|
27
|
+
function isPlainObject(value) {
|
|
28
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function requireNoExtraKeys(obj, allowed, where) {
|
|
31
|
+
for (const key of Object.keys(obj)) {
|
|
32
|
+
if (!allowed.includes(key))
|
|
33
|
+
throw new ApiError(400, 'invalid_request', `unexpected field in ${where}: ${key}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function invalid(message) {
|
|
37
|
+
return new ApiError(400, 'invalid_request', message);
|
|
38
|
+
}
|
|
39
|
+
function validatePage(raw) {
|
|
40
|
+
if (!isPlainObject(raw))
|
|
41
|
+
throw invalid('`page` must be an object');
|
|
42
|
+
requireNoExtraKeys(raw, ['dialect', 'source'], '`page`');
|
|
43
|
+
const dialect = raw['dialect'];
|
|
44
|
+
if (dialect !== 'jsx' && dialect !== 'html')
|
|
45
|
+
throw invalid('`page.dialect` must be "jsx" or "html"');
|
|
46
|
+
const source = raw['source'];
|
|
47
|
+
if (typeof source !== 'string' || source.trim() === '')
|
|
48
|
+
throw invalid('`page.source` must be the authored page text');
|
|
49
|
+
return { dialect, source };
|
|
50
|
+
}
|
|
51
|
+
/** Creation pins a programmatic request to the inbox with no bridge to reply to. */
|
|
52
|
+
function validateCreateDelivery(raw) {
|
|
53
|
+
if (raw === undefined)
|
|
54
|
+
return DEFAULT_DELIVERY;
|
|
55
|
+
if (!isPlainObject(raw))
|
|
56
|
+
throw invalid('`delivery` must be an object');
|
|
57
|
+
requireNoExtraKeys(raw, ['placement', 'inbox', 'reply'], '`delivery`');
|
|
58
|
+
const placement = validatePlacement(raw['placement'], DEFAULT_DELIVERY.placement);
|
|
59
|
+
const inbox = raw['inbox'] ?? true;
|
|
60
|
+
const reply = raw['reply'] ?? false;
|
|
61
|
+
if (inbox !== true)
|
|
62
|
+
throw invalid('`delivery.inbox` must be true: a programmatic request must stay answerable');
|
|
63
|
+
if (reply !== false)
|
|
64
|
+
throw invalid('`delivery.reply` must be false: a programmatic request has no agent bridge to reply to');
|
|
65
|
+
return { placement, inbox: true, reply: false };
|
|
66
|
+
}
|
|
67
|
+
/** Replace re-presents a pending page; where the ticket is routed is fixed at
|
|
68
|
+
* creation, so only placement is replaceable. */
|
|
69
|
+
function validateReplaceDelivery(raw, prior) {
|
|
70
|
+
if (raw === undefined)
|
|
71
|
+
return prior;
|
|
72
|
+
if (!isPlainObject(raw))
|
|
73
|
+
throw invalid('`delivery` must be an object');
|
|
74
|
+
requireNoExtraKeys(raw, ['placement'], '`delivery`');
|
|
75
|
+
return { ...prior, placement: validatePlacement(raw['placement'], prior.placement) };
|
|
76
|
+
}
|
|
77
|
+
function validatePlacement(raw, fallback) {
|
|
78
|
+
const placement = raw ?? fallback;
|
|
79
|
+
if (placement !== 'inline' && placement !== 'panel')
|
|
80
|
+
throw invalid('`delivery.placement` must be "inline" or "panel"');
|
|
81
|
+
return placement;
|
|
82
|
+
}
|
|
83
|
+
function validateSource(raw) {
|
|
84
|
+
if (raw === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (!isPlainObject(raw))
|
|
87
|
+
throw invalid('`source` must be an object');
|
|
88
|
+
requireNoExtraKeys(raw, SOURCE_FIELDS, '`source`');
|
|
89
|
+
const source = {};
|
|
90
|
+
for (const field of SOURCE_FIELDS) {
|
|
91
|
+
const value = raw[field];
|
|
92
|
+
if (value === undefined)
|
|
93
|
+
continue;
|
|
94
|
+
if (typeof value !== 'string' || value === '')
|
|
95
|
+
throw invalid(`\`source.${field}\` must be a nonempty string`);
|
|
96
|
+
source[field] = value;
|
|
97
|
+
}
|
|
98
|
+
return source;
|
|
99
|
+
}
|
|
100
|
+
function validateCreatorCwd(raw) {
|
|
101
|
+
if (typeof raw !== 'string' || raw === '')
|
|
102
|
+
throw invalid('`creator_cwd` is required');
|
|
103
|
+
if (!isAbsolute(raw))
|
|
104
|
+
throw invalid('`creator_cwd` must be an absolute path');
|
|
105
|
+
try {
|
|
106
|
+
if (!statSync(raw).isDirectory())
|
|
107
|
+
throw invalid(`\`creator_cwd\` is not a directory: ${raw}`);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof ApiError)
|
|
111
|
+
throw error;
|
|
112
|
+
throw invalid(`\`creator_cwd\` does not exist: ${raw}`);
|
|
113
|
+
}
|
|
114
|
+
return raw;
|
|
115
|
+
}
|
|
116
|
+
function validateActionSelection(raw) {
|
|
117
|
+
if (raw === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
if (!isPlainObject(raw))
|
|
120
|
+
throw invalid('`action` must be an object');
|
|
121
|
+
requireNoExtraKeys(raw, ['name', 'payload'], '`action`');
|
|
122
|
+
const name = raw['name'];
|
|
123
|
+
if (typeof name !== 'string' || !HUMAN_ACTION_NAME_PATTERN.test(name)) {
|
|
124
|
+
throw invalid('`action.name` must be a declared action name');
|
|
125
|
+
}
|
|
126
|
+
// The completion document always carries `action.payload`, so an omitted one
|
|
127
|
+
// is normalized here rather than serialized away downstream.
|
|
128
|
+
return { name, payload: raw['payload'] ?? null };
|
|
129
|
+
}
|
|
130
|
+
function validateCreateRequest(raw) {
|
|
131
|
+
if (!isPlainObject(raw))
|
|
132
|
+
throw invalid('request body is required');
|
|
133
|
+
requireNoExtraKeys(raw, ['page', 'delivery', 'source', 'creator_cwd', 'action'], 'the request body');
|
|
134
|
+
const source = validateSource(raw['source']);
|
|
135
|
+
const action = validateActionSelection(raw['action']);
|
|
136
|
+
return {
|
|
137
|
+
page: validatePage(raw['page']),
|
|
138
|
+
delivery: validateCreateDelivery(raw['delivery']),
|
|
139
|
+
...(source === undefined ? {} : { source }),
|
|
140
|
+
creatorCwd: validateCreatorCwd(raw['creator_cwd']),
|
|
141
|
+
...(action === undefined ? {} : { action }),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function validateReplaceRequest(raw, priorDelivery) {
|
|
145
|
+
if (!isPlainObject(raw))
|
|
146
|
+
throw invalid('request body is required');
|
|
147
|
+
requireNoExtraKeys(raw, ['page', 'delivery'], 'the request body');
|
|
148
|
+
return { page: validatePage(raw['page']), delivery: validateReplaceDelivery(raw['delivery'], priorDelivery) };
|
|
149
|
+
}
|
|
150
|
+
function optionalBoundedString(raw, field, max) {
|
|
151
|
+
if (raw === undefined)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (typeof raw !== 'string')
|
|
154
|
+
throw invalid(`\`${field}\` must be a string`);
|
|
155
|
+
if (raw.trim() === '' || raw.length > max)
|
|
156
|
+
throw invalid(`\`${field}\` must be nonempty after trim and at most ${max} characters`);
|
|
157
|
+
return raw;
|
|
158
|
+
}
|
|
159
|
+
function validateRespondRequest(raw) {
|
|
160
|
+
if (!isPlainObject(raw))
|
|
161
|
+
throw invalid('request body is required');
|
|
162
|
+
requireNoExtraKeys(raw, ['responses', 'actor'], 'the request body');
|
|
163
|
+
if (!isPlainObject(raw['responses']))
|
|
164
|
+
throw invalid('`responses` must be an object');
|
|
165
|
+
const actor = optionalBoundedString(raw['actor'], 'actor', ACTOR_MAX);
|
|
166
|
+
return { responses: raw['responses'], ...(actor === undefined ? {} : { actor }) };
|
|
167
|
+
}
|
|
168
|
+
function validateSettleRequest(raw) {
|
|
169
|
+
if (raw === undefined)
|
|
170
|
+
return {};
|
|
171
|
+
if (!isPlainObject(raw))
|
|
172
|
+
throw invalid('request body must be an object');
|
|
173
|
+
requireNoExtraKeys(raw, ['reason', 'actor'], 'the request body');
|
|
174
|
+
const reason = optionalBoundedString(raw['reason'], 'reason', REASON_MAX);
|
|
175
|
+
const actor = optionalBoundedString(raw['actor'], 'actor', ACTOR_MAX);
|
|
176
|
+
return { ...(reason === undefined ? {} : { reason }), ...(actor === undefined ? {} : { actor }) };
|
|
177
|
+
}
|
|
178
|
+
function resolveRequest(ctx) {
|
|
179
|
+
const requestId = requireTicketId({ ticket_id: ctx.params['request_id'] ?? '' });
|
|
180
|
+
const resolved = resolveTicket(requestId);
|
|
181
|
+
if (resolved === null || resolved.kind !== 'page')
|
|
182
|
+
throw new ApiError(404, 'not_found', 'no request with that id');
|
|
183
|
+
let manifest;
|
|
184
|
+
try {
|
|
185
|
+
manifest = parsePage(resolved.dir);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new ApiError(500, 'internal', 'the request could not be read');
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
requestId,
|
|
192
|
+
ticketId: resolved.ticketId,
|
|
193
|
+
dir: resolved.dir,
|
|
194
|
+
manifest,
|
|
195
|
+
result: readTicketResult(resolved.dir),
|
|
196
|
+
binding: readTicketActionBinding(resolved.dir),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function requestState(result) {
|
|
200
|
+
if (result === null)
|
|
201
|
+
return 'pending';
|
|
202
|
+
if (result.kind === 'canceled')
|
|
203
|
+
return completionEventFor(result);
|
|
204
|
+
return 'answered';
|
|
205
|
+
}
|
|
206
|
+
function deliveryDetail(row) {
|
|
207
|
+
if (row === null)
|
|
208
|
+
return { state: 'none', attempt: 0 };
|
|
209
|
+
const detail = { state: row.state, attempt: row.attempt };
|
|
210
|
+
// A next attempt is only meaningful for a pending retry; the enqueue instant
|
|
211
|
+
// of an untried delivery says nothing a client can act on.
|
|
212
|
+
if (row.state === 'pending' && row.lastFailure !== null)
|
|
213
|
+
detail.next_attempt_at = new Date(row.nextAttemptAt).toISOString();
|
|
214
|
+
if (row.acceptedAt !== null)
|
|
215
|
+
detail.accepted_at = new Date(row.acceptedAt).toISOString();
|
|
216
|
+
if (row.permanentFailedAt !== null)
|
|
217
|
+
detail.permanent_failed_at = new Date(row.permanentFailedAt).toISOString();
|
|
218
|
+
if (row.lastFailure !== null) {
|
|
219
|
+
const failure = row.lastFailure;
|
|
220
|
+
detail.last_failure = {
|
|
221
|
+
kind: failure.kind,
|
|
222
|
+
...(failure.exitCode === undefined ? {} : { exit_code: failure.exitCode }),
|
|
223
|
+
...(failure.signal === undefined ? {} : { signal: failure.signal }),
|
|
224
|
+
...(failure.message === undefined ? {} : { message: failure.message }),
|
|
225
|
+
...(failure.stderr === undefined ? {} : { stderr: failure.stderr }),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return detail;
|
|
229
|
+
}
|
|
230
|
+
function requestToDTO(request) {
|
|
231
|
+
const { manifest, result, binding } = request;
|
|
232
|
+
const emittedAt = manifest.source?.emittedAt;
|
|
233
|
+
if (emittedAt === undefined)
|
|
234
|
+
throw new ApiError(500, 'internal', 'the request carries no emitted time');
|
|
235
|
+
const state = requestState(result);
|
|
236
|
+
const dto = {
|
|
237
|
+
request_id: request.requestId,
|
|
238
|
+
state,
|
|
239
|
+
title: manifest.title,
|
|
240
|
+
source: manifest.source ?? {},
|
|
241
|
+
emitted_at: emittedAt,
|
|
242
|
+
};
|
|
243
|
+
if (manifest.subtitle !== undefined)
|
|
244
|
+
dto.subtitle = manifest.subtitle;
|
|
245
|
+
if (result !== null) {
|
|
246
|
+
dto.settled_at = result.kind === 'canceled' ? result.canceledAt : result.completedAt;
|
|
247
|
+
// Never fabricated for a dismissal or a withdrawal: only an answer has responses.
|
|
248
|
+
if (state === 'answered' && result.kind === 'page')
|
|
249
|
+
dto.responses = result.responses;
|
|
250
|
+
if (result.kind === 'canceled' && result.reason !== undefined)
|
|
251
|
+
dto.reason = result.reason;
|
|
252
|
+
const actor = result.kind === 'review' ? undefined : result.actor;
|
|
253
|
+
if (actor !== undefined)
|
|
254
|
+
dto.actor = actor;
|
|
255
|
+
}
|
|
256
|
+
if (binding !== null) {
|
|
257
|
+
dto.action = { name: binding.name, payload: binding.payload };
|
|
258
|
+
dto.delivery = deliveryDetail(readActionDelivery(request.requestId));
|
|
259
|
+
}
|
|
260
|
+
return dto;
|
|
261
|
+
}
|
|
262
|
+
// ===========================================================================
|
|
263
|
+
// POST /v1/human/requests — create
|
|
264
|
+
// ===========================================================================
|
|
265
|
+
/** Authoring failure is the caller's malformed page, never a server fault. */
|
|
266
|
+
function prepareOrReject(dialect, document, source, delivery) {
|
|
267
|
+
try {
|
|
268
|
+
return preparePageSource({ dialect, document, source, delivery, productKinds: resolvePageComponents() });
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
throw invalid(error instanceof Error ? error.message : 'the page could not be prepared');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function handleCreate(ctx) {
|
|
275
|
+
const request = validateCreateRequest(ctx.body);
|
|
276
|
+
// Action resolution precedes every write: a rejected action must leave no
|
|
277
|
+
// inbox row for a human to answer.
|
|
278
|
+
let resolvedAction;
|
|
279
|
+
if (request.action !== undefined) {
|
|
280
|
+
const resolution = resolveHumanAction(request.action.name, request.creatorCwd);
|
|
281
|
+
if ('kind' in resolution) {
|
|
282
|
+
const detail = resolution.kind === 'unknown_action'
|
|
283
|
+
? `no action named ${resolution.name} is declared from ${request.creatorCwd}`
|
|
284
|
+
: `${resolution.name} is declared in ${resolution.configOrigin} but unusable: ${resolution.reason}`;
|
|
285
|
+
throw new ApiError(400, resolution.kind, detail);
|
|
286
|
+
}
|
|
287
|
+
resolvedAction = resolution;
|
|
288
|
+
}
|
|
289
|
+
const prepared = prepareOrReject(request.page.dialect, request.page.source, request.source, request.delivery);
|
|
290
|
+
const dir = ticketDir(`page-${randomBytes(8).toString('hex')}`);
|
|
291
|
+
// The binding is written before the page exists, so a visible request is
|
|
292
|
+
// never momentarily unbound and `replace` can never reach the approved effect.
|
|
293
|
+
if (request.action !== undefined && resolvedAction !== undefined) {
|
|
294
|
+
publishTicketActionBinding(dir, {
|
|
295
|
+
schema: 'crtr.human-action-binding/v1',
|
|
296
|
+
name: request.action.name,
|
|
297
|
+
payload: request.action.payload,
|
|
298
|
+
creatorCwd: request.creatorCwd,
|
|
299
|
+
configOrigin: resolvedAction.configOrigin,
|
|
300
|
+
argv: resolvedAction.argv,
|
|
301
|
+
cwd: resolvedAction.cwd,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const published = publishPage(dir, prepared);
|
|
305
|
+
return {
|
|
306
|
+
status: 201,
|
|
307
|
+
body: {
|
|
308
|
+
request_id: opaqueInboxTicketId(basename(published.dir)),
|
|
309
|
+
state: 'pending',
|
|
310
|
+
...(request.action === undefined ? {} : { action: { name: request.action.name } }),
|
|
311
|
+
delivery_state: 'none',
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
// ===========================================================================
|
|
316
|
+
// GET /v1/human/requests/:request_id — read
|
|
317
|
+
// ===========================================================================
|
|
318
|
+
function handleGet(ctx) {
|
|
319
|
+
return { status: 200, body: requestToDTO(resolveRequest(ctx)) };
|
|
320
|
+
}
|
|
321
|
+
// ===========================================================================
|
|
322
|
+
// POST /v1/human/requests/:request_id/replace — revise the pending page
|
|
323
|
+
// ===========================================================================
|
|
324
|
+
function handleReplace(ctx) {
|
|
325
|
+
const request = resolveRequest(ctx);
|
|
326
|
+
if (request.result !== null)
|
|
327
|
+
throw new ApiError(409, 'already_settled', 'the request already has a terminal result');
|
|
328
|
+
const body = validateReplaceRequest(ctx.body, request.manifest.delivery);
|
|
329
|
+
// Identity and provenance are the creation's, verbatim — a revision changes
|
|
330
|
+
// only what is on screen, never who asked or when.
|
|
331
|
+
const prepared = prepareOrReject(body.page.dialect, body.page.source, request.manifest.source, body.delivery);
|
|
332
|
+
publishPage(request.dir, prepared);
|
|
333
|
+
return { status: 200, body: requestToDTO({ ...request, manifest: prepared.manifest }) };
|
|
334
|
+
}
|
|
335
|
+
// ===========================================================================
|
|
336
|
+
// Settlement — respond, dismiss, cancel
|
|
337
|
+
// ===========================================================================
|
|
338
|
+
/** The settlement funnels speak the inbox's error vocabulary; requests speak
|
|
339
|
+
* the wire contract's. This is the one translation. */
|
|
340
|
+
function settlementError(error) {
|
|
341
|
+
if (error instanceof ApiError) {
|
|
342
|
+
if (error.code === 'ticket_already_resolved')
|
|
343
|
+
return new ApiError(409, 'already_settled', 'the request already has a terminal result');
|
|
344
|
+
if (error.code === 'page_has_no_response_lifecycle')
|
|
345
|
+
return new ApiError(409, 'not_settleable', 'this request carries no response lifecycle');
|
|
346
|
+
return error;
|
|
347
|
+
}
|
|
348
|
+
if (error instanceof Error && error.message === 'page_has_no_response_lifecycle') {
|
|
349
|
+
return new ApiError(409, 'not_settleable', 'this request carries no response lifecycle');
|
|
350
|
+
}
|
|
351
|
+
return new ApiError(500, 'internal', 'the request could not be settled');
|
|
352
|
+
}
|
|
353
|
+
function requireSettleable(request) {
|
|
354
|
+
if (request.result !== null)
|
|
355
|
+
throw new ApiError(409, 'already_settled', 'the request already has a terminal result');
|
|
356
|
+
if (readPageTicket(request.dir).state === 'passive') {
|
|
357
|
+
throw new ApiError(409, 'not_settleable', 'this request carries no response lifecycle');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function handleRespond(ctx) {
|
|
361
|
+
const request = resolveRequest(ctx);
|
|
362
|
+
const body = validateRespondRequest(ctx.body);
|
|
363
|
+
requireSettleable(request);
|
|
364
|
+
try {
|
|
365
|
+
validatePageTicketResponses(request.dir, body.responses);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
// `requireSettleable` already refused a page with no response lifecycle, so
|
|
369
|
+
// anything left here is the caller's response map.
|
|
370
|
+
throw invalid(error instanceof Error ? error.message : 'the response map is invalid');
|
|
371
|
+
}
|
|
372
|
+
let result;
|
|
373
|
+
try {
|
|
374
|
+
result = await resolvePageTicket(request.ticketId, body.responses, body.actor === undefined ? {} : { actor: body.actor });
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
throw settlementError(error);
|
|
378
|
+
}
|
|
379
|
+
return { status: 200, body: requestToDTO({ ...request, result, binding: request.binding }) };
|
|
380
|
+
}
|
|
381
|
+
async function settle(ctx, disposition) {
|
|
382
|
+
const request = resolveRequest(ctx);
|
|
383
|
+
const body = validateSettleRequest(ctx.body);
|
|
384
|
+
requireSettleable(request);
|
|
385
|
+
let result;
|
|
386
|
+
try {
|
|
387
|
+
result = await cancelHumanTicket(request.ticketId, { ...body, disposition });
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
throw settlementError(error);
|
|
391
|
+
}
|
|
392
|
+
return { status: 200, body: requestToDTO({ ...request, result }) };
|
|
393
|
+
}
|
|
394
|
+
/** The recipient surface deliberately closing the request without answering. */
|
|
395
|
+
function handleDismiss(ctx) {
|
|
396
|
+
return settle(ctx, 'dismissed');
|
|
397
|
+
}
|
|
398
|
+
/** The requester withdrawing its own request. */
|
|
399
|
+
function handleCancel(ctx) {
|
|
400
|
+
return settle(ctx, 'canceled');
|
|
401
|
+
}
|
|
402
|
+
export const humanRequestRoutes = [
|
|
403
|
+
{ method: 'POST', pattern: '/v1/human/requests', handler: handleCreate },
|
|
404
|
+
{ method: 'GET', pattern: '/v1/human/requests/:request_id', handler: handleGet },
|
|
405
|
+
{ method: 'POST', pattern: '/v1/human/requests/:request_id/replace', handler: handleReplace },
|
|
406
|
+
{ method: 'POST', pattern: '/v1/human/requests/:request_id/respond', handler: handleRespond },
|
|
407
|
+
{ method: 'POST', pattern: '/v1/human/requests/:request_id/dismiss', handler: handleDismiss },
|
|
408
|
+
{ method: 'POST', pattern: '/v1/human/requests/:request_id/cancel', handler: handleCancel },
|
|
409
|
+
];
|
|
@@ -108,6 +108,9 @@ async function handleCancel(ctx) {
|
|
|
108
108
|
if (request !== undefined && (typeof request !== 'object' || request === null)) {
|
|
109
109
|
throw new ApiError(400, 'invalid_request', 'ticket cancel body must be an object');
|
|
110
110
|
}
|
|
111
|
+
if (request?.disposition !== undefined && request.disposition !== 'canceled' && request.disposition !== 'dismissed') {
|
|
112
|
+
throw new ApiError(400, 'invalid_request', '`disposition` must be "canceled" or "dismissed"');
|
|
113
|
+
}
|
|
111
114
|
const dir = ticketDir(ticketId);
|
|
112
115
|
const review = getReviewByBridge(ticketId);
|
|
113
116
|
try {
|
|
@@ -364,6 +364,9 @@ async function handleCancel(ctx) {
|
|
|
364
364
|
const result = await cancelHumanTicket(resolved.ticketId, {
|
|
365
365
|
reason: request.reason ?? DEFAULT_CANCEL_REASON,
|
|
366
366
|
actor: 'human',
|
|
367
|
+
// This is the recipient surface: closing a request here is a dismissal,
|
|
368
|
+
// never the requester withdrawing it.
|
|
369
|
+
disposition: 'dismissed',
|
|
367
370
|
});
|
|
368
371
|
if (result.kind !== 'canceled')
|
|
369
372
|
throw new ApiError(409, 'ticket_already_resolved', 'ticket was already resolved');
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { type ModelRegistryLike } from '../../../core/model-routes.js';
|
|
2
|
-
import { assertProfileActive } from '../../../core/profiles/manifest.js';
|
|
3
2
|
import type { HandlerResult, RequestContext, RouteTable } from '../router.js';
|
|
4
3
|
interface NodeHandlerDeps {
|
|
5
4
|
modelRegistry?: ModelRegistryLike;
|
|
6
5
|
}
|
|
7
|
-
export
|
|
6
|
+
export { resolveCreateKind } from '../../../core/runtime/launch-target.js';
|
|
8
7
|
export declare function handleCreate(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
9
8
|
export declare function handleList(ctx: RequestContext): HandlerResult;
|
|
10
9
|
export declare function handlePromote(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
11
10
|
export declare function handleYield(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
12
11
|
export declare function handleConfig(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
13
12
|
export declare const nodeRoutes: RouteTable;
|
|
14
|
-
export {};
|