@parall/cli 1.37.0 → 1.39.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/dist/commands/chats.d.ts.map +1 -1
- package/dist/commands/chats.js +45 -2
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +45 -4
- package/dist/commands/tasks.d.ts.map +1 -1
- package/dist/commands/tasks.js +23 -2
- package/dist/lib/client.d.ts +61 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +173 -1
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chats.d.ts","sourceRoot":"","sources":["../../src/commands/chats.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chats.d.ts","sourceRoot":"","sources":["../../src/commands/chats.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAwMpD"}
|
package/dist/commands/chats.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ApiError } from '@parall/sdk';
|
|
1
2
|
import { resolveCredentials } from '../lib/client.js';
|
|
2
3
|
import { printJson, printError, printRefHint, stripPrllScheme } from '../lib/output.js';
|
|
3
4
|
export function registerChatCommands(program) {
|
|
@@ -70,6 +71,23 @@ export function registerChatCommands(program) {
|
|
|
70
71
|
printError(err);
|
|
71
72
|
}
|
|
72
73
|
});
|
|
74
|
+
chats
|
|
75
|
+
.command('transfer-ownership')
|
|
76
|
+
.description('Transfer chat ownership to another member (owner only; you become admin)')
|
|
77
|
+
.argument('<chatId>', 'Chat ID')
|
|
78
|
+
.requiredOption('--user-id <userId>', 'Chat member to become the new owner')
|
|
79
|
+
.action(async (chatId, opts) => {
|
|
80
|
+
try {
|
|
81
|
+
const { client, orgId } = resolveCredentials();
|
|
82
|
+
await client.transferChatOwnership(orgId, stripPrllScheme(chatId), {
|
|
83
|
+
target_user_id: stripPrllScheme(opts.userId),
|
|
84
|
+
});
|
|
85
|
+
printJson({ ok: true });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
printError(err);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
73
91
|
chats
|
|
74
92
|
.command('create')
|
|
75
93
|
.description('Create a new chat')
|
|
@@ -111,16 +129,41 @@ export function registerChatCommands(program) {
|
|
|
111
129
|
printError(err);
|
|
112
130
|
}
|
|
113
131
|
});
|
|
132
|
+
members
|
|
133
|
+
.command('set-role')
|
|
134
|
+
.description('Change a chat member role to admin or member (transferring ownership has its own command)')
|
|
135
|
+
.argument('<chatId>', 'Chat ID')
|
|
136
|
+
.requiredOption('--user-id <userId>', 'Target member')
|
|
137
|
+
.requiredOption('--role <role>', 'New role: admin or member')
|
|
138
|
+
.action(async (chatId, opts) => {
|
|
139
|
+
try {
|
|
140
|
+
if (opts.role !== 'admin' && opts.role !== 'member') {
|
|
141
|
+
// Local pre-check with the same structured envelope printError emits
|
|
142
|
+
// for server errors, so scripted callers always get status + code.
|
|
143
|
+
throw new ApiError(400, opts.role === 'owner'
|
|
144
|
+
? 'Use "chats transfer-ownership" to change the owner'
|
|
145
|
+
: '--role must be admin or member', 'INVALID_REQUEST');
|
|
146
|
+
}
|
|
147
|
+
const { client, orgId } = resolveCredentials();
|
|
148
|
+
await client.updateChatMemberRole(orgId, stripPrllScheme(chatId), stripPrllScheme(opts.userId), opts.role);
|
|
149
|
+
printJson({ ok: true });
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
printError(err);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
114
155
|
members
|
|
115
156
|
.command('add')
|
|
116
157
|
.description('Add a member to a chat')
|
|
117
158
|
.argument('<chatId>', 'Chat ID')
|
|
118
159
|
.requiredOption('--user-id <userId>', 'User ID to add')
|
|
119
|
-
.option('--role <role>', 'Deprecated.
|
|
160
|
+
.option('--role <role>', 'Deprecated. New members always join as "member"')
|
|
120
161
|
.action(async (chatId, opts) => {
|
|
121
162
|
try {
|
|
122
163
|
if (opts.role !== undefined && opts.role !== 'member') {
|
|
123
|
-
throw new
|
|
164
|
+
throw new ApiError(400, opts.role === 'owner'
|
|
165
|
+
? 'New members always join as "member"; add them first, then use "chats transfer-ownership"'
|
|
166
|
+
: '--role only supports "member"; promote after adding via "chats members set-role"', 'INVALID_REQUEST');
|
|
124
167
|
}
|
|
125
168
|
const { client, orgId } = resolveCredentials();
|
|
126
169
|
await client.addChatMember(orgId, stripPrllScheme(chatId), stripPrllScheme(opts.userId));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QA8MvD"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiError } from '@parall/sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { LaneContextError, markLaneReplyCommitted, resolveCredentials, resolveLaneDispatchContext, resolveRuntimeContext, } from '../lib/client.js';
|
|
3
4
|
import { printError, printJson, printRefHint, stripPrllScheme } from '../lib/output.js';
|
|
4
5
|
import { NO_BODY_ERROR, resolveMessageText, TEXT_FILE_OPTION_DESC, TEXT_OPTION_DESC, } from '../lib/text-input.js';
|
|
5
6
|
import { uploadFile } from '../lib/upload.js';
|
|
@@ -98,19 +99,59 @@ export function registerMessageCommands(program) {
|
|
|
98
99
|
};
|
|
99
100
|
if (attachmentIds)
|
|
100
101
|
req.attachment_ids = attachmentIds;
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
const threadRootId = opts.threadRootId !== undefined ? stripPrllScheme(opts.threadRootId) : undefined;
|
|
103
|
+
if (threadRootId !== undefined)
|
|
104
|
+
req.thread_root_id = threadRootId;
|
|
103
105
|
if (opts.reply === false)
|
|
104
106
|
req.hints = { no_reply: true };
|
|
105
107
|
if (ctx.stepId)
|
|
106
108
|
req.agent_step_id = ctx.stepId;
|
|
107
109
|
if (ctx.sessionId)
|
|
108
110
|
req.agent_session_id = ctx.sessionId;
|
|
109
|
-
|
|
111
|
+
// Dispatch lane binding (PRLL_CONTEXT_DIR contract): a send whose
|
|
112
|
+
// exact (chat, thread) target has an active lane context rides the
|
|
113
|
+
// server's dispatch ledger — the lane token authorizes the write,
|
|
114
|
+
// and the FIRST conversational reply of the lane carries the
|
|
115
|
+
// reply:<dispatch_event_id> effect key so it resolves the dispatched
|
|
116
|
+
// work in the same transaction. Later sends in the same turn (and
|
|
117
|
+
// cross-chat sends, which find no lane context) use a random
|
|
118
|
+
// per-invocation idempotency key. All of this stays out of the
|
|
119
|
+
// model's view.
|
|
120
|
+
const laneCtx = resolveLaneDispatchContext(chatId, threadRootId);
|
|
121
|
+
let usedReplyKey = false;
|
|
122
|
+
if (laneCtx) {
|
|
123
|
+
req.dispatch_lane = laneCtx.lane;
|
|
124
|
+
if (laneCtx.dispatchEventId && !laneCtx.replyCommitted) {
|
|
125
|
+
req.idempotency_key = `reply:${laneCtx.dispatchEventId}`;
|
|
126
|
+
usedReplyKey = true;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
req.idempotency_key = `cli:${randomUUID()}`;
|
|
130
|
+
}
|
|
131
|
+
if (!req.agent_step_id && laneCtx.stepId)
|
|
132
|
+
req.agent_step_id = laneCtx.stepId;
|
|
133
|
+
if (!req.agent_session_id && laneCtx.sessionId)
|
|
134
|
+
req.agent_session_id = laneCtx.sessionId;
|
|
135
|
+
}
|
|
136
|
+
const { message: result, deduplicated } = await client.sendMessageDetailed(orgId, chatId, req);
|
|
137
|
+
if (laneCtx && usedReplyKey) {
|
|
138
|
+
markLaneReplyCommitted(laneCtx);
|
|
139
|
+
}
|
|
110
140
|
printJson(result);
|
|
141
|
+
if (deduplicated && usedReplyKey) {
|
|
142
|
+
console.error('Note: this reply was already sent by a previous run (deduplicated) — do not resend it.');
|
|
143
|
+
}
|
|
111
144
|
printRefHint(result.id, 'Sent');
|
|
112
145
|
}
|
|
113
146
|
catch (err) {
|
|
147
|
+
if (err instanceof ApiError && err.code === 'STALE_LANE') {
|
|
148
|
+
printError(new ApiError(err.status, 'Your processing slot was taken over by a newer run — stop working on this conversation.', 'STALE_LANE'));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (err instanceof LaneContextError) {
|
|
152
|
+
printError(err);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
114
155
|
printError(err);
|
|
115
156
|
}
|
|
116
157
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAsWpD"}
|
package/dist/commands/tasks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ApiError } from '@parall/sdk';
|
|
2
|
+
import { resolveCredentials, resolveTypedDispatchBinding } from '../lib/client.js';
|
|
2
3
|
import { printJson, printError, printRefHint, stripPrllScheme } from '../lib/output.js';
|
|
3
4
|
function stripUndefined(obj) {
|
|
4
5
|
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
@@ -113,10 +114,30 @@ export function registerTaskCommands(program) {
|
|
|
113
114
|
if (Object.keys(data).length === 0) {
|
|
114
115
|
printError(new Error('No fields to update. Provide at least one option.'));
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
+
// Typed dispatch binding: when this turn is a typed dispatch about
|
|
118
|
+
// exactly this task, the update rides the claimed typed lane; the
|
|
119
|
+
// first update also carries the canonical task_update:<dsp> effect
|
|
120
|
+
// key so it resolves the dispatched work in the same transaction.
|
|
121
|
+
// Invisible to the model — same contract as message replies.
|
|
122
|
+
const binding = resolveTypedDispatchBinding(taskId);
|
|
123
|
+
const req = { ...data };
|
|
124
|
+
if (binding) {
|
|
125
|
+
req.dispatch_lane = binding.lane;
|
|
126
|
+
req.dispatch_event_id = binding.dispatchEventId;
|
|
127
|
+
if (binding.effectKey)
|
|
128
|
+
req.dispatch_effect_key = binding.effectKey;
|
|
129
|
+
}
|
|
130
|
+
const result = await client.updateTask(orgId, taskId, req);
|
|
131
|
+
if (binding?.effectKey) {
|
|
132
|
+
binding.markCommitted();
|
|
133
|
+
}
|
|
117
134
|
printJson(result);
|
|
118
135
|
}
|
|
119
136
|
catch (err) {
|
|
137
|
+
if (err instanceof ApiError && err.code === 'STALE_LANE') {
|
|
138
|
+
printError(new ApiError(err.status, 'Your processing slot was taken over by a newer run — stop working on this task.', 'STALE_LANE'));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
120
141
|
printError(err);
|
|
121
142
|
}
|
|
122
143
|
});
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export type RuntimeContext = {
|
|
|
11
11
|
triggerMessageId?: string;
|
|
12
12
|
/** When true, CLI should not send automatic reply messages — the bridge handles text projection. */
|
|
13
13
|
noReply: boolean;
|
|
14
|
+
/** Dispatch ledger binding of the current turn (additive; bridge-written). */
|
|
15
|
+
dispatchEventId?: string;
|
|
16
|
+
lane?: string;
|
|
17
|
+
laneTargetUri?: string;
|
|
18
|
+
/** Typed binding hint: the task this dispatch is about (task dispatch turns only). */
|
|
19
|
+
taskId?: string;
|
|
14
20
|
};
|
|
15
21
|
/**
|
|
16
22
|
* Read per-dispatch context from ENV + sideband files.
|
|
@@ -21,5 +27,60 @@ export type RuntimeContext = {
|
|
|
21
27
|
* 3. PRLL_STEP_ID_FILE plain text (legacy step-id-only backward compat)
|
|
22
28
|
*/
|
|
23
29
|
export declare function resolveRuntimeContext(): RuntimeContext;
|
|
30
|
+
/** Typed dispatch binding for a task write, derived from the turn context. */
|
|
31
|
+
export type TypedDispatchBinding = {
|
|
32
|
+
lane: string;
|
|
33
|
+
dispatchEventId: string;
|
|
34
|
+
/** Set for the first (resolving) update of this dispatch. */
|
|
35
|
+
effectKey?: string;
|
|
36
|
+
markCommitted: () => void;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the typed-lane binding for `parall task update <taskId>`: the
|
|
40
|
+
* current turn must be a typed dispatch (lane target `dsp:<id>`) about this
|
|
41
|
+
* exact task. The first update carries the canonical
|
|
42
|
+
* `task_update:<dispatch_event_id>` effect key (resolves the WorkItem in the
|
|
43
|
+
* same transaction); later updates go keyless (lane-checked only). The
|
|
44
|
+
* committed flag lives in a CLI-owned sidecar keyed by the typed lane; when
|
|
45
|
+
* PRLL_CONTEXT_DIR is absent the sidecar can't exist, so no effect key is
|
|
46
|
+
* ever attached (a blind key would make a *different* second update replay
|
|
47
|
+
* the first one's result instead of applying).
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveTypedDispatchBinding(taskId: string): TypedDispatchBinding | null;
|
|
50
|
+
/** Per-lane dispatch context (PRLL_CONTEXT_DIR contract), keyed by send target. */
|
|
51
|
+
export type LaneDispatchContext = {
|
|
52
|
+
lane: string;
|
|
53
|
+
dispatchEventId?: string;
|
|
54
|
+
targetUri: string;
|
|
55
|
+
threadRootId?: string;
|
|
56
|
+
sessionId?: string;
|
|
57
|
+
stepId?: string;
|
|
58
|
+
/** True when the reply slot for this lane was already consumed (sidecar). */
|
|
59
|
+
replyCommitted: boolean;
|
|
60
|
+
replyStatePath: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Error thrown when a lane context file exists for the send target but lacks
|
|
64
|
+
* the dispatch/lane fields — the strong-idempotency path must fail closed
|
|
65
|
+
* rather than silently degrade to a best-effort write.
|
|
66
|
+
*/
|
|
67
|
+
export declare class LaneContextError extends Error {
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Look up the dispatch lane context for a send target under the
|
|
71
|
+
* PRLL_CONTEXT_DIR contract. Returns null when the contract is not in effect
|
|
72
|
+
* (env unset) or no lane file exists for this exact (target, thread) — i.e.
|
|
73
|
+
* a cross-chat / non-dispatch send, which stays on the best-effort path.
|
|
74
|
+
*
|
|
75
|
+
* The lane key encodes the full lane identity: a thread send only matches a
|
|
76
|
+
* thread lane, a channel send only a channel lane.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveLaneDispatchContext(chatId: string, threadRootId?: string): LaneDispatchContext | null;
|
|
79
|
+
/**
|
|
80
|
+
* Record that this lane's first conversational reply has been committed. The
|
|
81
|
+
* sidecar is CLI-owned — never the bridge-owned context file, which the
|
|
82
|
+
* bridge rewrites throughout the turn and would clobber inline state.
|
|
83
|
+
*/
|
|
84
|
+
export declare function markLaneReplyCommitted(laneCtx: LaneDispatchContext): void;
|
|
24
85
|
export declare function resolveCredentials(): ResolvedCredentials;
|
|
25
86
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/lib/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oGAAoG;IACpG,OAAO,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAmDtD;AAED,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAwCvF;AAED,mFAAmF;AACnF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,GACpB,mBAAmB,GAAG,IAAI,CA+E5B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAYzE;AAED,wBAAgB,kBAAkB,IAAI,mBAAmB,CA8BxD"}
|
package/dist/lib/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
+
import { laneContextFilePath, laneReplyStateFilePath } from '@parall/agent-core';
|
|
2
3
|
import { ParallClient } from '@parall/sdk';
|
|
3
4
|
/**
|
|
4
5
|
* Read per-dispatch context from ENV + sideband files.
|
|
@@ -14,6 +15,10 @@ export function resolveRuntimeContext() {
|
|
|
14
15
|
let chatId = process.env.PRLL_CHAT_ID?.trim() || undefined;
|
|
15
16
|
let triggerMessageId = process.env.PRLL_TRIGGER_MESSAGE_ID?.trim() || undefined;
|
|
16
17
|
let noReply = process.env.PRLL_NO_REPLY === '1';
|
|
18
|
+
let dispatchEventId;
|
|
19
|
+
let lane;
|
|
20
|
+
let laneTargetUri;
|
|
21
|
+
let taskId;
|
|
17
22
|
if (process.env.PRLL_CONTEXT_FILE) {
|
|
18
23
|
try {
|
|
19
24
|
const raw = fs.readFileSync(process.env.PRLL_CONTEXT_FILE, 'utf-8').trim();
|
|
@@ -25,6 +30,10 @@ export function resolveRuntimeContext() {
|
|
|
25
30
|
stepId ??= ctx.step_id || undefined;
|
|
26
31
|
if (ctx.no_reply === true)
|
|
27
32
|
noReply = true;
|
|
33
|
+
dispatchEventId = ctx.dispatch_event_id || undefined;
|
|
34
|
+
lane = ctx.lane || undefined;
|
|
35
|
+
laneTargetUri = ctx.target_uri || undefined;
|
|
36
|
+
taskId = ctx.task_id || undefined;
|
|
28
37
|
}
|
|
29
38
|
}
|
|
30
39
|
catch {
|
|
@@ -41,7 +50,170 @@ export function resolveRuntimeContext() {
|
|
|
41
50
|
// File not yet written or unreadable — no step context
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
sessionId,
|
|
55
|
+
stepId,
|
|
56
|
+
chatId,
|
|
57
|
+
triggerMessageId,
|
|
58
|
+
noReply,
|
|
59
|
+
dispatchEventId,
|
|
60
|
+
lane,
|
|
61
|
+
laneTargetUri,
|
|
62
|
+
taskId,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the typed-lane binding for `parall task update <taskId>`: the
|
|
67
|
+
* current turn must be a typed dispatch (lane target `dsp:<id>`) about this
|
|
68
|
+
* exact task. The first update carries the canonical
|
|
69
|
+
* `task_update:<dispatch_event_id>` effect key (resolves the WorkItem in the
|
|
70
|
+
* same transaction); later updates go keyless (lane-checked only). The
|
|
71
|
+
* committed flag lives in a CLI-owned sidecar keyed by the typed lane; when
|
|
72
|
+
* PRLL_CONTEXT_DIR is absent the sidecar can't exist, so no effect key is
|
|
73
|
+
* ever attached (a blind key would make a *different* second update replay
|
|
74
|
+
* the first one's result instead of applying).
|
|
75
|
+
*/
|
|
76
|
+
export function resolveTypedDispatchBinding(taskId) {
|
|
77
|
+
const ctx = resolveRuntimeContext();
|
|
78
|
+
if (!ctx.lane || !ctx.dispatchEventId)
|
|
79
|
+
return null;
|
|
80
|
+
if (!ctx.laneTargetUri?.startsWith('dsp:'))
|
|
81
|
+
return null;
|
|
82
|
+
if (!ctx.taskId || ctx.taskId !== taskId)
|
|
83
|
+
return null;
|
|
84
|
+
const binding = {
|
|
85
|
+
lane: ctx.lane,
|
|
86
|
+
dispatchEventId: ctx.dispatchEventId,
|
|
87
|
+
markCommitted: () => { },
|
|
88
|
+
};
|
|
89
|
+
const contextDir = process.env.PRLL_CONTEXT_DIR?.trim();
|
|
90
|
+
if (!contextDir)
|
|
91
|
+
return binding;
|
|
92
|
+
const sidecarPath = laneReplyStateFilePath(contextDir, ctx.laneTargetUri);
|
|
93
|
+
let committed = false;
|
|
94
|
+
try {
|
|
95
|
+
const sidecar = JSON.parse(fs.readFileSync(sidecarPath, 'utf-8'));
|
|
96
|
+
committed =
|
|
97
|
+
sidecar.reply_committed === true && sidecar.dispatch_event_id === ctx.dispatchEventId;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// No sidecar yet — resolving update not committed.
|
|
101
|
+
}
|
|
102
|
+
if (!committed) {
|
|
103
|
+
const dispatchEventId = ctx.dispatchEventId;
|
|
104
|
+
binding.effectKey = `task_update:${dispatchEventId}`;
|
|
105
|
+
binding.markCommitted = () => {
|
|
106
|
+
try {
|
|
107
|
+
fs.writeFileSync(sidecarPath, JSON.stringify({ dispatch_event_id: dispatchEventId, reply_committed: true }), 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Best-effort — a lost sidecar means the next update retries the key
|
|
111
|
+
// and the server's effect ledger replies idempotently.
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return binding;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Error thrown when a lane context file exists for the send target but lacks
|
|
119
|
+
* the dispatch/lane fields — the strong-idempotency path must fail closed
|
|
120
|
+
* rather than silently degrade to a best-effort write.
|
|
121
|
+
*/
|
|
122
|
+
export class LaneContextError extends Error {
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Look up the dispatch lane context for a send target under the
|
|
126
|
+
* PRLL_CONTEXT_DIR contract. Returns null when the contract is not in effect
|
|
127
|
+
* (env unset) or no lane file exists for this exact (target, thread) — i.e.
|
|
128
|
+
* a cross-chat / non-dispatch send, which stays on the best-effort path.
|
|
129
|
+
*
|
|
130
|
+
* The lane key encodes the full lane identity: a thread send only matches a
|
|
131
|
+
* thread lane, a channel send only a channel lane.
|
|
132
|
+
*/
|
|
133
|
+
export function resolveLaneDispatchContext(chatId, threadRootId) {
|
|
134
|
+
const contextDir = process.env.PRLL_CONTEXT_DIR?.trim();
|
|
135
|
+
if (!contextDir)
|
|
136
|
+
return null;
|
|
137
|
+
const targetUri = `prll://${chatId}`;
|
|
138
|
+
const filePath = laneContextFilePath(contextDir, targetUri, threadRootId);
|
|
139
|
+
let raw;
|
|
140
|
+
try {
|
|
141
|
+
raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// Only a genuinely-absent file means "not a dispatch-bound send".
|
|
145
|
+
// Permission errors etc. must fail closed, not silently degrade the
|
|
146
|
+
// write out of the ledger.
|
|
147
|
+
const code = err.code;
|
|
148
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
149
|
+
return null;
|
|
150
|
+
throw new LaneContextError(`Dispatch context for ${targetUri} is unreadable (${filePath}) — retry, or escalate if this persists`);
|
|
151
|
+
}
|
|
152
|
+
if (!raw) {
|
|
153
|
+
throw new LaneContextError(`Dispatch context for ${targetUri} is empty (${filePath}) — retry, or escalate if this persists`);
|
|
154
|
+
}
|
|
155
|
+
let ctx;
|
|
156
|
+
try {
|
|
157
|
+
ctx = JSON.parse(raw);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
throw new LaneContextError(`Dispatch context for ${targetUri} is unreadable (${filePath}) — retry, or escalate if this persists`);
|
|
161
|
+
}
|
|
162
|
+
const lane = typeof ctx.lane === 'string' && ctx.lane ? ctx.lane : undefined;
|
|
163
|
+
if (!lane) {
|
|
164
|
+
// File exists but the lane fields are missing: fail closed instead of
|
|
165
|
+
// silently writing a side effect outside the ledger.
|
|
166
|
+
throw new LaneContextError(`Dispatch context for ${targetUri} is missing its lane binding — retry, or escalate if this persists`);
|
|
167
|
+
}
|
|
168
|
+
const dispatchEventId = typeof ctx.dispatch_event_id === 'string' && ctx.dispatch_event_id
|
|
169
|
+
? ctx.dispatch_event_id
|
|
170
|
+
: undefined;
|
|
171
|
+
if (!dispatchEventId) {
|
|
172
|
+
// The bridge folds every group member before dispatching (fail-closed
|
|
173
|
+
// ensureLane), so a lane context without its trigger WorkItem id is
|
|
174
|
+
// corruption — never a legitimate degraded state.
|
|
175
|
+
throw new LaneContextError(`Dispatch context for ${targetUri} is missing its dispatch_event_id — retry, or escalate if this persists`);
|
|
176
|
+
}
|
|
177
|
+
const replyStatePath = laneReplyStateFilePath(contextDir, targetUri, threadRootId);
|
|
178
|
+
let replyCommitted = false;
|
|
179
|
+
try {
|
|
180
|
+
const sidecar = JSON.parse(fs.readFileSync(replyStatePath, 'utf-8'));
|
|
181
|
+
// The sidecar only counts for the same trigger — a new turn (new
|
|
182
|
+
// dispatch_event_id) resets the reply slot.
|
|
183
|
+
replyCommitted =
|
|
184
|
+
sidecar.reply_committed === true &&
|
|
185
|
+
dispatchEventId != null &&
|
|
186
|
+
sidecar.dispatch_event_id === dispatchEventId;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// No sidecar yet — reply slot unused.
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
lane,
|
|
193
|
+
dispatchEventId,
|
|
194
|
+
targetUri,
|
|
195
|
+
threadRootId,
|
|
196
|
+
sessionId: typeof ctx.session_id === 'string' && ctx.session_id ? ctx.session_id : undefined,
|
|
197
|
+
stepId: typeof ctx.step_id === 'string' && ctx.step_id ? ctx.step_id : undefined,
|
|
198
|
+
replyCommitted,
|
|
199
|
+
replyStatePath,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Record that this lane's first conversational reply has been committed. The
|
|
204
|
+
* sidecar is CLI-owned — never the bridge-owned context file, which the
|
|
205
|
+
* bridge rewrites throughout the turn and would clobber inline state.
|
|
206
|
+
*/
|
|
207
|
+
export function markLaneReplyCommitted(laneCtx) {
|
|
208
|
+
if (!laneCtx.dispatchEventId)
|
|
209
|
+
return;
|
|
210
|
+
try {
|
|
211
|
+
fs.writeFileSync(laneCtx.replyStatePath, JSON.stringify({ dispatch_event_id: laneCtx.dispatchEventId, reply_committed: true }), 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Best-effort — a lost sidecar means the next send retries the reply key
|
|
215
|
+
// and the server's effect ledger dedupes it.
|
|
216
|
+
}
|
|
45
217
|
}
|
|
46
218
|
export function resolveCredentials() {
|
|
47
219
|
const url = process.env.PRLL_API_URL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "CLI client for Parall — universal agent & human access to Parall API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,13 +36,14 @@
|
|
|
36
36
|
"diff": "^8.0.3",
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
38
|
"zod": "^4.3.6",
|
|
39
|
-
"@parall/
|
|
39
|
+
"@parall/agent-core": "1.39.0",
|
|
40
|
+
"@parall/sdk": "1.39.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/js-yaml": "^4.0.9",
|
|
43
44
|
"@types/node": "^22.0.0",
|
|
44
45
|
"typescript": "^5.7.0",
|
|
45
|
-
"@parall/agent-core": "1.
|
|
46
|
+
"@parall/agent-core": "1.39.0"
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
|
48
49
|
"build": "tsc",
|