@hachej/boring-agent 0.4.29 → 0.4.30
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 +4 -0
- package/dist/artifacts.d.ts +11 -1
- package/dist/artifacts.js +22 -2
- package/dist/task-artifacts.d.ts +22 -0
- package/dist/task-artifacts.js +34 -0
- package/dist/view-open.d.ts +13 -0
- package/dist/view-open.js +32 -10
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -56,6 +56,10 @@ Limits: a run is admitted against its hold, not stopped mid-run (no per-call fen
|
|
|
56
56
|
|
|
57
57
|
## Artifacts
|
|
58
58
|
|
|
59
|
+
`present` also accepts exactly one host-owned task reference: `{ task: 'D-201' }` or `{ tasks: { filter: 'all' | 'mine' | 'waiting-on-me' } }`. These calls need no workspace. They return `{ kind: 'presented', artifact: { schema: 'boring.task', version: 1, type: 'task', title, subject } }`, with no file target, revision or task payload. The host authenticates and resolves `subject`; `mine` uses its authenticated person, never a model-supplied principal. `parsePresentedArtifact` accepts both file and task descriptors; the existing `parseArtifact` and `ArtifactDescriptor` stay file-only. Browser contracts are exported by `@hachej/boring-ui-kit/chat-view`.
|
|
60
|
+
|
|
61
|
+
With `ack`, task presentation uses the same durable wait and outcomes as files. The page reports `{ kind: 'opened', instanceId, subject }` only after the exact authorized task or filtered list loads and mounts, or `{ kind: 'refused', reason }`. A different task, filter or file-shaped report is refused. This confirms a view, not task completion, consent, permission or a mutation. Without a mount report it remains `unconfirmed`. [`AmbientChat` host viewers](../../registry/README.md#host-task-viewers) receive `onShown`/`onFailed` through the second argument of `artifacts.open`; existing file calls and one-argument host openers remain valid.
|
|
62
|
+
|
|
59
63
|
`createPresentTool({ workspace, resolveAccess?, maxBytes? })` from `@hachej/boring-agent/artifacts` returns the native `present(path)` tool (its workspace resolved per call like the guard's): the agent writes a workspace file with its ordinary file tools and presents it, and the chat shows a card that points at that file (`target`, the revision shown, no `id` or `ordinal`). The file's versions are the history of the workspace provider (`@hachej/boring-files/workspace`, whose `keep` retains the presented revision); a card of an older revision pins it and a card of the newest follows the file as it changes. The tool writes nothing and refuses files that are missing, not UTF-8 text or larger than 256 KiB. An opt-in `ack: { timeoutMs?, viewers? }` makes `present` wait until a page has mounted the presented revision: the page answers `{ kind: 'opened', instanceId, path, revision }` or `{ kind: 'refused', reason }` through the chat's `answer` (the transport's `?op=answer`), and the result carries `opened: true` with `viewer`, `opened: false` with `refused` (for example `closed by user`), `opened: 'unconfirmed'` after `timeoutMs` (default 15 s) or `opened: false, viewer: 'none'` at once when `viewers()` says no page watches the conversation. The wait is the durable wait of `ask_user` (`packages/agent/src/view-open.ts`, a conversation document keyed by the call); the deadline is stored, so a restart cannot extend it, and the first valid answer wins. Without `ack` the result is unchanged. `answerViewOpen` settles an answer; the host chains it with `answerUserQuestion` and `answerBrowserTask` in its `answer` handler. How to use it is in the tool's own description, so a host does not repeat it in its prompt: substantial output goes to a file that is then presented, short answers (and anything asked for in the chat) stay in the chat, and a revision re-reads the file, changes it at the same path and presents it again. A host adds only what is its own, such as what its HTML preview allows.
|
|
60
64
|
|
|
61
65
|
### A view that is not a document (a dashboard)
|
package/dist/artifacts.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { Type } from '@earendil-works/pi-ai';
|
|
|
3
3
|
import type { Context } from '@earendil-works/chord';
|
|
4
4
|
import type { ResourceAccess, ResourceLocator } from '@hachej/boring-files';
|
|
5
5
|
import type { WorkspaceBinding, WorkspaceResolver } from './workspaces.js';
|
|
6
|
+
import type { TaskArtifactDescriptor } from './task-artifacts.js';
|
|
7
|
+
export { createTaskArtifact, parseTaskArtifact, parseTaskSubject, taskSubjectKey } from './task-artifacts.js';
|
|
8
|
+
export type { TaskArtifactDescriptor, TaskSubject } from './task-artifacts.js';
|
|
9
|
+
export type PresentedArtifact = ArtifactDescriptor | TaskArtifactDescriptor;
|
|
10
|
+
/** Parse file or host-task presentation without treating a record as a workspace file. */
|
|
11
|
+
export declare function parsePresentedArtifact(value: unknown): PresentedArtifact | undefined;
|
|
6
12
|
import type { ViewOpenOptions } from './view-open.js';
|
|
7
13
|
export { answerViewOpen, parseViewOpenAnswer, VIEW_OPEN_TIMEOUT_MS } from './view-open.js';
|
|
8
14
|
export type { ViewOpenAnswer, ViewOpenOptions, ViewOpenOutcome } from './view-open.js';
|
|
@@ -57,6 +63,10 @@ export interface PresentToolOptions {
|
|
|
57
63
|
* tools first. The card points at the file; the file's history (the provider's) is its list of versions. Nothing is written here.
|
|
58
64
|
*/
|
|
59
65
|
export declare function createPresentTool(options?: PresentToolOptions): import("@earendil-works/pi-durable").ToolRegistration<Type.TObject<{
|
|
60
|
-
path: Type.TString
|
|
66
|
+
path: Type.TOptional<Type.TString>;
|
|
67
|
+
task: Type.TOptional<Type.TString>;
|
|
68
|
+
tasks: Type.TOptional<Type.TObject<{
|
|
69
|
+
filter: Type.TUnion<[Type.TLiteral<"all">, Type.TLiteral<"mine">, Type.TLiteral<"waiting-on-me">]>;
|
|
70
|
+
}>>;
|
|
61
71
|
}>, import("@earendil-works/chord").JsonValue>;
|
|
62
72
|
//# sourceMappingURL=artifacts.d.ts.map
|
package/dist/artifacts.js
CHANGED
|
@@ -2,7 +2,11 @@ import { defineTool } from '@earendil-works/pi-durable';
|
|
|
2
2
|
import { Type } from '@earendil-works/pi-ai';
|
|
3
3
|
import { workspacePath } from '@hachej/boring-files/file-types';
|
|
4
4
|
import { asWorkspaceResolver, workspaceFor } from './workspaces.js';
|
|
5
|
-
import {
|
|
5
|
+
import { createTaskArtifact, parseTaskArtifact, parseTaskSubject } from './task-artifacts.js';
|
|
6
|
+
export { createTaskArtifact, parseTaskArtifact, parseTaskSubject, taskSubjectKey } from './task-artifacts.js';
|
|
7
|
+
/** Parse file or host-task presentation without treating a record as a workspace file. */
|
|
8
|
+
export function parsePresentedArtifact(value) { return parseArtifact(value) ?? parseTaskArtifact(value); }
|
|
9
|
+
import { PRESENT_TOOL, awaitTaskViewOpen, awaitViewOpen } from './view-open.js';
|
|
6
10
|
export { answerViewOpen, parseViewOpenAnswer, VIEW_OPEN_TIMEOUT_MS } from './view-open.js';
|
|
7
11
|
/*
|
|
8
12
|
* Artifacts: substantial, self-contained content (a document, an HTML page, an SVG, code) that a chat shows as a card
|
|
@@ -100,14 +104,30 @@ export function createPresentTool(options = {}) {
|
|
|
100
104
|
name: PRESENT_TOOL,
|
|
101
105
|
// How to use it travels with the tool, so every host that offers `present` gets the same rules without repeating them in its prompt.
|
|
102
106
|
description: [
|
|
107
|
+
'Show a host task with { task: "D-201" }, or a task list with { tasks: { filter: "all", "mine" or "waiting-on-me" } }. Tasks are host records, not workspace files; the host checks access when it opens them.',
|
|
103
108
|
'Show a file of the workspace to the person in a side panel instead of pasting it in your reply.',
|
|
104
109
|
'When the person asks for something substantial and self-contained (a report, document, HTML page, SVG image or program), write it to a file with the write tool, using a short descriptive path with the right extension (.md, .html, .svg with a viewBox, or the language\'s own), then call present with that path; never put that content in your reply. After present, reply with one or two short sentences and never repeat the content.',
|
|
105
110
|
'Short answers stay in the chat, and so does anything the person asks to have in the chat or without tools or artifacts: then call no tool.',
|
|
106
111
|
'To change, extend or fix something you presented: read the file again first (the person may have edited it; keep their changes), change it with edit (or write the whole new content) at the same path, never a second file, and call present again. If you do not remember the path, list the files.',
|
|
107
112
|
].join(' '),
|
|
108
|
-
parameters: Type.Object({
|
|
113
|
+
parameters: Type.Object({
|
|
114
|
+
path: Type.Optional(Type.String({ minLength: 1, maxLength: 512, description: 'A workspace file path. Supply exactly one of path, task or tasks.' })),
|
|
115
|
+
task: Type.Optional(Type.String({ minLength: 1, maxLength: 128, pattern: '^[A-Za-z0-9][A-Za-z0-9_-]*$', description: 'A host task id or number, such as D-201.' })),
|
|
116
|
+
tasks: Type.Optional(Type.Object({ filter: Type.Union([Type.Literal('all'), Type.Literal('mine'), Type.Literal('waiting-on-me')]) }, { additionalProperties: false })),
|
|
117
|
+
}, { additionalProperties: false }),
|
|
109
118
|
replay: 'safe',
|
|
110
119
|
execute: async (args, api, context) => {
|
|
120
|
+
if (Object.keys(args).length !== 1)
|
|
121
|
+
return reply({ kind: 'denied', reason: 'Supply exactly one of path, task or tasks' });
|
|
122
|
+
if (args.path === undefined) {
|
|
123
|
+
const subject = parseTaskSubject(args);
|
|
124
|
+
if (!subject)
|
|
125
|
+
return reply({ kind: 'denied', reason: 'Invalid task reference' });
|
|
126
|
+
if (!options.ack)
|
|
127
|
+
return reply({ kind: 'presented', artifact: createTaskArtifact(subject) });
|
|
128
|
+
const shown = await awaitTaskViewOpen(api, context, { tool: PRESENT_TOOL, subject }, options.ack);
|
|
129
|
+
return reply({ kind: 'presented', artifact: createTaskArtifact(shown.subject), ...viewFields(shown.outcome) });
|
|
130
|
+
}
|
|
111
131
|
// An absolute path is refused, not normalised: `/workspace/a.md` must never be read as `workspace/a.md` under the root.
|
|
112
132
|
if (args.path.startsWith('/'))
|
|
113
133
|
return reply({ kind: 'denied', reason: 'The path must be relative to the workspace' });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A host-owned task reference, not a file, execution task, permission or mutation. */
|
|
2
|
+
export type TaskSubject = {
|
|
3
|
+
readonly task: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly tasks: {
|
|
6
|
+
readonly filter: 'all' | 'mine' | 'waiting-on-me';
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export interface TaskArtifactDescriptor {
|
|
10
|
+
readonly schema: 'boring.task';
|
|
11
|
+
readonly version: 1;
|
|
12
|
+
readonly type: 'task';
|
|
13
|
+
readonly title: string;
|
|
14
|
+
readonly subject: TaskSubject;
|
|
15
|
+
}
|
|
16
|
+
/** Strict, detached reference parsing; the host still authenticates and resolves the task. */
|
|
17
|
+
export declare function parseTaskSubject(value: unknown): TaskSubject | undefined;
|
|
18
|
+
/** The browser and server use the same exact subject identity for presentation acknowledgements. */
|
|
19
|
+
export declare function taskSubjectKey(subject: TaskSubject): string;
|
|
20
|
+
export declare function parseTaskArtifact(value: unknown): TaskArtifactDescriptor | undefined;
|
|
21
|
+
export declare function createTaskArtifact(subject: TaskSubject): TaskArtifactDescriptor;
|
|
22
|
+
//# sourceMappingURL=task-artifacts.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
2
|
+
const only = (value, keys) => Object.keys(value).every(key => keys.includes(key));
|
|
3
|
+
/** Strict, detached reference parsing; the host still authenticates and resolves the task. */
|
|
4
|
+
export function parseTaskSubject(value) {
|
|
5
|
+
if (!object(value))
|
|
6
|
+
return undefined;
|
|
7
|
+
if (only(value, ['task']) && typeof value['task'] === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value['task']))
|
|
8
|
+
return { task: value['task'] };
|
|
9
|
+
const tasks = value['tasks'];
|
|
10
|
+
if (!only(value, ['tasks']) || !object(tasks) || !only(tasks, ['filter']))
|
|
11
|
+
return undefined;
|
|
12
|
+
const filter = tasks['filter'];
|
|
13
|
+
return filter === 'all' || filter === 'mine' || filter === 'waiting-on-me' ? { tasks: { filter } } : undefined;
|
|
14
|
+
}
|
|
15
|
+
/** The browser and server use the same exact subject identity for presentation acknowledgements. */
|
|
16
|
+
export function taskSubjectKey(subject) {
|
|
17
|
+
return 'task' in subject ? JSON.stringify(['task', subject.task]) : JSON.stringify(['tasks', subject.tasks.filter]);
|
|
18
|
+
}
|
|
19
|
+
export function parseTaskArtifact(value) {
|
|
20
|
+
if (!object(value) || !only(value, ['schema', 'version', 'type', 'title', 'subject']))
|
|
21
|
+
return undefined;
|
|
22
|
+
if (value['schema'] !== 'boring.task' || value['version'] !== 1 || value['type'] !== 'task')
|
|
23
|
+
return undefined;
|
|
24
|
+
const title = value['title'], subject = parseTaskSubject(value['subject']);
|
|
25
|
+
if (typeof title !== 'string' || !title.trim() || title.length > 200 || !subject)
|
|
26
|
+
return undefined;
|
|
27
|
+
return { schema: 'boring.task', version: 1, type: 'task', title, subject };
|
|
28
|
+
}
|
|
29
|
+
export function createTaskArtifact(subject) {
|
|
30
|
+
const parsed = parseTaskSubject(subject);
|
|
31
|
+
if (!parsed)
|
|
32
|
+
throw new TypeError('Invalid task subject');
|
|
33
|
+
return { schema: 'boring.task', version: 1, type: 'task', title: 'task' in parsed ? parsed.task : parsed.tasks.filter === 'mine' ? 'My requests' : parsed.tasks.filter === 'waiting-on-me' ? 'Waiting on me' : 'Tasks', subject: parsed };
|
|
34
|
+
}
|
package/dist/view-open.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Conversation, ToolExecutionApi } from '@earendil-works/pi-durable';
|
|
2
2
|
import type { Context } from '@earendil-works/chord';
|
|
3
3
|
import type { AskUserAnswer } from './ask-user.js';
|
|
4
|
+
import type { TaskSubject } from './task-artifacts.js';
|
|
4
5
|
/** How long a view may take to be confirmed by a page before the result says `unconfirmed` (a UI ack, not a person's decision). */
|
|
5
6
|
export declare const VIEW_OPEN_TIMEOUT_MS = 15000;
|
|
6
7
|
/** The tool whose calls the view-open record belongs to, unless a host renamed `present`. */
|
|
@@ -11,6 +12,10 @@ export type ViewOpenAnswer = {
|
|
|
11
12
|
readonly instanceId: string;
|
|
12
13
|
readonly path: string;
|
|
13
14
|
readonly revision: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'opened';
|
|
17
|
+
readonly instanceId: string;
|
|
18
|
+
readonly subject: TaskSubject;
|
|
14
19
|
} | {
|
|
15
20
|
readonly kind: 'refused';
|
|
16
21
|
readonly reason: string;
|
|
@@ -50,6 +55,14 @@ export declare function awaitViewOpen(api: ToolExecutionApi, context: Context, v
|
|
|
50
55
|
readonly outcome: ViewOpenOutcome;
|
|
51
56
|
readonly revision: string;
|
|
52
57
|
}>;
|
|
58
|
+
/** A record viewer confirms the exact task or filter, never a fabricated file revision. */
|
|
59
|
+
export declare function awaitTaskViewOpen(api: ToolExecutionApi, context: Context, view: {
|
|
60
|
+
readonly tool: string;
|
|
61
|
+
readonly subject: TaskSubject;
|
|
62
|
+
}, options?: ViewOpenOptions): Promise<{
|
|
63
|
+
readonly outcome: ViewOpenOutcome;
|
|
64
|
+
readonly subject: TaskSubject;
|
|
65
|
+
}>;
|
|
53
66
|
/**
|
|
54
67
|
* Settles a pending view with the page's answer (JSON text) after the host authenticated the person. `id` is the answer ID of the
|
|
55
68
|
* call (`[assistantEntryId, callId]`, or a raw call ID used by one task). An `opened` answer must name the path and revision the
|
package/dist/view-open.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { defineDocFamily } from '@earendil-works/pi-durable';
|
|
7
7
|
import { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';
|
|
8
8
|
import { AmbiguousCall, UnknownCall, callBinding, ownBindingKey, waitForDocument } from './durable-wait.js';
|
|
9
|
+
import { parseTaskSubject, taskSubjectKey } from './task-artifacts.js';
|
|
9
10
|
/** How long a view may take to be confirmed by a page before the result says `unconfirmed` (a UI ack, not a person's decision). */
|
|
10
11
|
export const VIEW_OPEN_TIMEOUT_MS = 15_000;
|
|
11
12
|
/** The tool whose calls the view-open record belongs to, unless a host renamed `present`. */
|
|
@@ -32,6 +33,12 @@ export function parseViewOpenAnswer(value) {
|
|
|
32
33
|
return undefined;
|
|
33
34
|
return { kind: 'opened', instanceId, path, revision };
|
|
34
35
|
}
|
|
36
|
+
if (value['kind'] === 'opened' && Object.keys(value).every(key => ['kind', 'instanceId', 'subject'].includes(key))) {
|
|
37
|
+
const instanceId = value['instanceId'], subject = parseTaskSubject(value['subject']);
|
|
38
|
+
if (typeof instanceId !== 'string' || !instanceId || instanceId.length > MAX_ID || !subject)
|
|
39
|
+
return undefined;
|
|
40
|
+
return { kind: 'opened', instanceId, subject };
|
|
41
|
+
}
|
|
35
42
|
if (value['kind'] === 'refused' && Object.keys(value).every(key => ['kind', 'reason'].includes(key))) {
|
|
36
43
|
const { reason } = value;
|
|
37
44
|
if (typeof reason !== 'string' || !reason || reason.length > MAX_REASON)
|
|
@@ -47,16 +54,29 @@ export function parseViewOpenAnswer(value) {
|
|
|
47
54
|
* current one, so the result never pairs a new file revision with an old acknowledgement.
|
|
48
55
|
*/
|
|
49
56
|
export async function awaitViewOpen(api, context, view, options = {}) {
|
|
57
|
+
const result = await awaitPresentation(api, context, view, options);
|
|
58
|
+
if (!('revision' in result.binding))
|
|
59
|
+
throw new Error('The presentation belongs to a task reference');
|
|
60
|
+
return { outcome: result.outcome, revision: result.binding.revision };
|
|
61
|
+
}
|
|
62
|
+
/** A record viewer confirms the exact task or filter, never a fabricated file revision. */
|
|
63
|
+
export async function awaitTaskViewOpen(api, context, view, options = {}) {
|
|
64
|
+
const result = await awaitPresentation(api, context, view, options);
|
|
65
|
+
if (!('subject' in result.binding))
|
|
66
|
+
throw new Error('The presentation belongs to a file');
|
|
67
|
+
return { outcome: result.outcome, subject: result.binding.subject };
|
|
68
|
+
}
|
|
69
|
+
async function awaitPresentation(api, context, view, options) {
|
|
50
70
|
const timeoutMs = options.timeoutMs ?? VIEW_OPEN_TIMEOUT_MS;
|
|
51
71
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
|
|
52
72
|
throw new TypeError('The view-open timeout must be a positive integer of milliseconds');
|
|
53
73
|
if (options.viewers && !(await options.viewers(api, context)))
|
|
54
|
-
return { outcome: { kind: 'none' },
|
|
74
|
+
return { outcome: { kind: 'none' }, binding: view };
|
|
55
75
|
const { key, startedAt, duration } = await api.commit(async (tx) => {
|
|
56
76
|
const key = await ownBindingKey(tx, api);
|
|
57
77
|
const document = await tx.doc(documents, api.conversationId, key, null);
|
|
58
78
|
if (!document.view)
|
|
59
|
-
document.view = {
|
|
79
|
+
document.view = { ...view, taskId: api.taskId, startedAt: Date.now(), timeoutMs, state: { kind: 'pending' } };
|
|
60
80
|
if (document.view.taskId !== api.taskId)
|
|
61
81
|
throw new Error('View-open identity belongs to another task');
|
|
62
82
|
return { key, startedAt: document.view.startedAt, duration: document.view.timeoutMs };
|
|
@@ -73,13 +93,13 @@ export async function awaitViewOpen(api, context, view, options = {}) {
|
|
|
73
93
|
document.view.state = { kind: 'cancelled' };
|
|
74
94
|
}, 'View open', { at: startedAt + duration, expire });
|
|
75
95
|
const state = final?.view?.state;
|
|
76
|
-
const
|
|
96
|
+
const binding = final?.view ?? view;
|
|
77
97
|
if (state?.kind === 'opened')
|
|
78
|
-
return { outcome: { kind: 'opened', viewer: state.viewer },
|
|
98
|
+
return { outcome: { kind: 'opened', viewer: state.viewer }, binding };
|
|
79
99
|
if (state?.kind === 'refused')
|
|
80
|
-
return { outcome: { kind: 'refused', reason: state.reason },
|
|
100
|
+
return { outcome: { kind: 'refused', reason: state.reason }, binding };
|
|
81
101
|
if (state?.kind === 'unconfirmed')
|
|
82
|
-
return { outcome: { kind: 'unconfirmed', timeoutMs: duration },
|
|
102
|
+
return { outcome: { kind: 'unconfirmed', timeoutMs: duration }, binding };
|
|
83
103
|
throw new Error('View open cancelled');
|
|
84
104
|
}
|
|
85
105
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
@@ -104,7 +124,7 @@ export async function answerViewOpen(conversation, id, answer, options = {}) {
|
|
|
104
124
|
}
|
|
105
125
|
const parsed = parseViewOpenAnswer(value);
|
|
106
126
|
if (!parsed)
|
|
107
|
-
return { kind: 'denied', reason: 'The answer must
|
|
127
|
+
return { kind: 'denied', reason: 'The answer must name an opened file revision or task subject with instanceId, or be { kind: "refused", reason }' };
|
|
108
128
|
const next = parsed.kind === 'opened' ? { kind: 'opened', viewer: parsed.instanceId } : { kind: 'refused', reason: parsed.reason };
|
|
109
129
|
try {
|
|
110
130
|
return await conversation.commit(async (tx) => {
|
|
@@ -119,6 +139,11 @@ export async function answerViewOpen(conversation, id, answer, options = {}) {
|
|
|
119
139
|
const record = document.view;
|
|
120
140
|
if (record.taskId !== task.id)
|
|
121
141
|
return { kind: 'conflict', reason: 'The view belongs to another task' };
|
|
142
|
+
if (parsed.kind === 'opened' && ('subject' in parsed
|
|
143
|
+
? !('subject' in record) || taskSubjectKey(parsed.subject) !== taskSubjectKey(record.subject)
|
|
144
|
+
: !('path' in record) || parsed.path.replace(/^\.\//, '') !== record.path || parsed.revision !== record.revision)) {
|
|
145
|
+
return { kind: 'denied', reason: 'The acknowledgement names another subject or revision than the one presented' };
|
|
146
|
+
}
|
|
122
147
|
if (record.state.kind !== 'pending')
|
|
123
148
|
return same(record.state, next) ? { kind: 'answered' } : { kind: 'conflict', reason: 'The view was already decided' };
|
|
124
149
|
if (task.abortRequested || task.state.status === 'terminal') {
|
|
@@ -129,9 +154,6 @@ export async function answerViewOpen(conversation, id, answer, options = {}) {
|
|
|
129
154
|
record.state = { kind: 'unconfirmed' };
|
|
130
155
|
return { kind: 'conflict', reason: 'The view was not confirmed in time' };
|
|
131
156
|
}
|
|
132
|
-
if (parsed.kind === 'opened' && (parsed.path.replace(/^\.\//, '') !== record.path || parsed.revision !== record.revision)) {
|
|
133
|
-
return { kind: 'denied', reason: 'The acknowledgement names another file or revision than the one presented' };
|
|
134
|
-
}
|
|
135
157
|
record.state = next;
|
|
136
158
|
return { kind: 'answered' };
|
|
137
159
|
}, options.context ?? BACKGROUND_CONTEXT);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -180,8 +180,8 @@
|
|
|
180
180
|
"@earendil-works/pi-durable": "~1.4.0",
|
|
181
181
|
"@earendil-works/pi-ai": "~1.4.0",
|
|
182
182
|
"@earendil-works/chord": "~1.4.0",
|
|
183
|
-
"@hachej/boring-files": "^0.4.
|
|
184
|
-
"@hachej/boring-ui-kit": "^0.4.
|
|
183
|
+
"@hachej/boring-files": "^0.4.30",
|
|
184
|
+
"@hachej/boring-ui-kit": "^0.4.30",
|
|
185
185
|
"typebox": "1.3.27",
|
|
186
186
|
"xlsx": "^0.20.3"
|
|
187
187
|
},
|