sync: update from private repo (ddadfd71)
CI / build-and-test (push) Waiting to run

This commit is contained in:
oss-sync
2026-07-08 23:35:00 +00:00
parent b1292e34b2
commit 77ee3bc426
187 changed files with 19918 additions and 10938 deletions
+20 -29
View File
@@ -12,16 +12,20 @@
*
* Scoping is enforced by the bound `ctx.taskConversation` IO: comments come from
* `listLocalTaskComments(taskId)` (WHERE task_id = ?), and the transcript path is
* bound by the worker ONLY to a per-task `runtimeDir` — never the space-shared
* `workspacePath/logs` fallback — so a tool call can never read another task's
* logs. When the transcript path is unset the transcript source yields nothing
* (comments still work); when the whole IO is unset (subtasks / gitea-issue runs)
* the tools degrade to a friendly non-error message.
* bound by the worker to a location a tool call can never use to read another
* task's logs. For a top-level local task that is the per-task `runtimeDir`
* (never the space-shared `workspacePath/logs` fallback). A sub-task run instead
* gets a transcript-only IO (no comments — see `makeTranscriptOnlyConversationIO`)
* bound to the sub-task's OWN isolated workspace transcript. When the transcript
* path is unset the transcript source yields nothing (comments still work); when
* the whole IO is unset (gitea-issue runs) the tools degrade to a friendly
* non-error message.
*/
import { existsSync, readFileSync } from 'node:fs';
import { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult, TaskConversationComment } from './core.js';
import { excerpt, tokenize } from './text-excerpt.js';
const SEARCH_DEF: ToolDef = {
type: 'function',
@@ -32,7 +36,7 @@ const SEARCH_DEF: ToolDef = {
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: '検索キーワード (部分一致・大文字小文字無視)。' },
query: { type: 'string', description: '検索キーワード。スペース区切りで複数指定すると AND 検索 (すべての語を含む発言にヒット)。部分一致・大文字小文字無視。' },
source: { type: 'string', enum: ['comments', 'transcript', 'both'], description: '検索対象。既定 both。' },
author: { type: 'string', enum: ['user', 'agent', 'system'], description: '発言者で絞り込む。' },
kind: { type: 'string', description: 'コメント種別で絞り込む (request/comment/interjection/result/ask/progress/handoff)。transcript には適用されない。' },
@@ -105,23 +109,6 @@ function loadTranscript(path: string | undefined): TranscriptEntry[] {
return out;
}
function collapse(s: string): string {
return s.replace(/\s+/g, ' ').trim();
}
/** Compact excerpt centered on the first query hit, bounded to `max` chars. */
function excerpt(body: string, query: string, max: number): string {
const flat = collapse(body);
if (flat.length <= max && !query) return flat;
const idx = query ? flat.toLowerCase().indexOf(query.toLowerCase()) : 0;
if (idx < 0 || flat.length <= max) {
return flat.length <= max ? flat : flat.slice(0, max - 1) + '…';
}
const start = Math.max(0, idx - 40);
const end = Math.min(flat.length, start + max);
return (start > 0 ? '…' : '') + flat.slice(start, end) + (end < flat.length ? '…' : '');
}
function clampLimit(v: unknown, def: number, max: number): number {
const n = typeof v === 'number' && Number.isFinite(v) ? Math.floor(v) : def;
return Math.min(Math.max(n, 1), max);
@@ -144,7 +131,11 @@ async function search(input: Record<string, unknown>, ctx: ToolContext): Promise
const author = input['author'] as Author | undefined;
const kind = typeof input['kind'] === 'string' ? (input['kind'] as string) : undefined;
const limit = clampLimit(input['limit'], 10, 50);
const q = query.toLowerCase();
const tokens = tokenize(query);
const matches = (body: string): boolean => {
const lower = body.toLowerCase();
return tokens.every((t) => lower.includes(t));
};
type Hit = { ref: string; header: string; body: string };
const hits: Hit[] = [];
@@ -154,7 +145,7 @@ async function search(input: Record<string, unknown>, ctx: ToolContext): Promise
for (const c of comments) {
if (author && c.author !== author) continue;
if (kind && c.kind !== kind) continue;
if (!c.body.toLowerCase().includes(q)) continue;
if (!matches(c.body)) continue;
hits.push({ ref: `comment:${c.id}`, header: `${c.author}/${c.kind} ${c.createdAt}`, body: c.body });
}
}
@@ -165,7 +156,7 @@ async function search(input: Record<string, unknown>, ctx: ToolContext): Promise
if (!kind) {
for (const e of loadTranscript(io.transcriptPath)) {
if (author && e.author !== author) continue;
if (!e.content.toLowerCase().includes(q)) continue;
if (!matches(e.content)) continue;
hits.push({ ref: `transcript:${e.index}`, header: e.author, body: e.content });
}
}
@@ -179,7 +170,7 @@ async function search(input: Record<string, unknown>, ctx: ToolContext): Promise
const lines = [`## Conversation Search Results (query: "${query}", ${hits.length} hit(s)${hits.length > limit ? `, showing ${limit}` : ''})`, ''];
for (const h of shown) {
lines.push(`- ${h.ref} ${h.header}`);
lines.push(` ${excerpt(h.body, query, EXCERPT_MAX)}`);
lines.push(` ${excerpt(h.body, tokens, EXCERPT_MAX)}`);
lines.push('');
}
lines.push('前後の文脈は ReadTaskConversation({ ref }) で確認できます。');
@@ -209,7 +200,7 @@ async function read(input: Record<string, unknown>, ctx: ToolContext): Promise<T
for (const c of slice) {
const marker = c.id === id ? ' ←' : '';
lines.push(`- comment:${c.id} ${c.author}/${c.kind} ${c.createdAt}${marker}`);
lines.push(` ${excerpt(c.body, '', READ_EXCERPT_MAX)}`);
lines.push(` ${excerpt(c.body, [], READ_EXCERPT_MAX)}`);
}
return { output: lines.join('\n'), isError: false };
}
@@ -222,7 +213,7 @@ async function read(input: Record<string, unknown>, ctx: ToolContext): Promise<T
for (const e of slice) {
const marker = e.index === id ? ' ←' : '';
lines.push(`- transcript:${e.index} ${e.author}${marker}`);
lines.push(` ${excerpt(e.content, '', READ_EXCERPT_MAX)}`);
lines.push(` ${excerpt(e.content, [], READ_EXCERPT_MAX)}`);
}
return { output: lines.join('\n'), isError: false };
}